@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.
@@ -14,6 +14,42 @@ Update the display name with `ours-cowork room settings <room-id> --name "New na
14
14
 
15
15
  Runtime commands are default-deny. Per-CID grants use `command-grant` and `command-revoke`. An operator may instead register a durable role policy with `role-command-set <room-id> --role <label> --commands <comma-list>` and inspect it with `role-command-grants`; use `--commands none` to remove it. A role policy authorizes only an authenticated, active seat whose durable admission role exactly matches the policy. Display names, message labels, pending seats, removed seats, and caller-supplied role text never confer authority. Removing a role policy does not remove an independently configured per-CID grant.
16
16
 
17
+ ## Scoped reply threads
18
+
19
+ The dedicated `start_thread` runtime command creates a scoped reply thread after an operator grants that exact command name. Discover and invoke it through the SDK's generic command APIs, then use the SDK's native reply field:
20
+
21
+ ```ts
22
+ const definitions = await client.listContactCommands({ contact: roomCid });
23
+ const start = definitions.find(c => c.name === 'start_thread');
24
+ if (!start) throw new Error('This room does not advertise start_thread');
25
+ await client.sendCommand({ contact: roomCid, command: start.name,
26
+ arguments: { topic: 'Review', participant_ids: selectedParticipantIds,
27
+ idempotency_key: stableKeyForThisCreation } });
28
+ // Receive the room's root normally; reply using YOUR received root wire ID.
29
+ await client.sendMessage({ contact: roomCid, text: 'My reply',
30
+ reply_to_wire_id: receivedRoot.wire_id });
31
+ // Omitting reply_to_wire_id creates an ordinary whole-room message.
32
+ ```
33
+
34
+ Here, `client` is an existing bound Ours client and `roomCid` is its room contact CID. Obtain `selectedParticipantIds` from the room's separately permitted `list-members` roster command; they are the chosen stable participant IDs and must include the creator. `stableKeyForThisCreation` is an opaque client-generated key reused only for identical retries. `receivedRoot` is this member's incoming copy of the root message, not another member's copy.
35
+
36
+ The input object has exactly `topic`, `participant_ids`, and `idempotency_key`. A topic contains 1–120 Unicode code points, contains at least one non-whitespace character, excludes Unicode control and format characters, and is trimmed after the raw value passes those bounds. An idempotency key contains 1–128 ASCII characters from `A-Z`, `a-z`, `0-9`, `.`, `_`, `:`, and `-`. The participant list contains one or more distinct, active roster IDs. A one-member thread is valid; its replies are archived and acknowledged with no other recipient.
37
+
38
+ The authenticated creator must be active, selected, and granted `start_thread`. Creation records the selected seats as immutable pairs of participant ID and CID. Later admissions are not backfilled, and removing then re-admitting a CID does not restore access under its new participant ID. Roots and replies go only to selected original seats that are still active. A member does not need a `start_thread` grant to reply after receiving a root. Each recipient replies to its own received wire ID; Cowork translates each relayed reply to that recipient's copy of the immediate parent.
39
+
40
+ A successful command result is `{"ok":true,"thread_id":"<thread-id>","status":"accepted"}`. Acceptance means the root and its immutable recipient work were stored; delivery to each selected member proceeds independently and may be partial. Repeating the same creator key with the same normalized topic and participant set, in any participant order, returns the original receipt. Use these public errors without relying on diagnostic details:
41
+
42
+ | Error | Meaning |
43
+ | --- | --- |
44
+ | `unauthorized` | The caller is not an active granted creator, or the same creator/key belongs to an earlier seat incarnation. |
45
+ | `invalid_request` | The command object or one of its bounded fields does not match the advertised schema. |
46
+ | `invalid_members` | A selected ID is duplicate, unknown, inactive, or does not include the creator. |
47
+ | `idempotency_conflict` | The creator reused a key with a different normalized topic or participant set. |
48
+ | `reply_target_unavailable` | A reply target is missing, invalid, expired, ambiguous, foreign to the sender, or no longer authorized. The content is rejected and is never broadcast. |
49
+ | `thread_files_unsupported` | A file targets a scoped root or descendant; version one scoped threads carry messages only. |
50
+
51
+ Invalid scoped replies and scoped file attempts are durably rejected before their inbox item is acknowledged. Cowork makes one best-effort attempt to send the submitting member a private, fixed error-code notice; that notice can be lost, and a restart does not repeat it. The rejected content, target, and routing details are not included in the notice. If Cowork cannot prove a selected recipient's local parent copy during relay or restart, that recipient's delivery ends with a terminal unavailable result; private content is not sent without its reply link.
52
+
17
53
  Membership changes are deliberately independent operator actions. Add a participant by issuing an invite for the intended role and admitting that identity; remove a participant with `ours-cowork room remove <room-id> <participant>`. To preserve coverage, add and confirm the new participant before removing the old one. To remove a dead participant first, remove it and issue a new invite afterward. Cowork does not combine these actions into a replacement operation or infer successor lineage.
18
54
 
19
55
  Participant removal has no durable intent or result phase. Cowork asks the shared daemon to remove the contact, treats an already-absent exact contact as completion, and then records the seat as removed. If the daemon completed removal but its response or the following metadata save was lost, repeat the same remove command; the retry observes the absent contact and finishes the local update. Old prerelease `membership_intent` and `membership_result` history records remain readable but are inert: reconciliation, rebind, messaging, close, and deletion never replay them.
@@ -32,7 +68,7 @@ Room-scoped operations also appear in the room identity's ours catalog with thei
32
68
 
33
69
  Use the RPC arguments without `room_id`; the receiving room fixes the target. For example, an operator grants `ours-cowork room command-grant <room-id> <caller-cid> room.settings`, then that active member calls `room.settings` with `{"status":"review"}` using ours command transport. A grant for one name grants none of the other names. The SDK returns a correlated result containing `{ok:true,result:<service value>}` or `{ok:false,error:<code>}`. History returns one page; follow `seq` with `after` to fetch more.
34
70
 
35
- `list-members` retains its contact-safe roster. `remove-member` retains its epoch, confirm and no-self-removal gates. The separate `room.participant.remove` command instead grants the full operator removal behavior. `room.show`, `room.participants` and operator-view history return operator metadata. Policy-administration commands can delegate more privileges; `room.message` and `room.say` authorize room/role authorship. Assign these permissions deliberately. Command results may include invite material; do not relay them into chat.
71
+ `start_thread`, `list-members`, and `remove-member` are dedicated runtime commands rather than shared management routes. `list-members` retains its contact-safe roster. `remove-member` retains its epoch, confirm and no-self-removal gates. The separate `room.participant.remove` command instead grants the full operator removal behavior. `room.show` returns only public room settings and mission content; `room.participants` returns participant IDs, roles, and states. Runtime `room.history` returns only messages visible to the authenticated active seat, with viewer-local cursors, and rejects operator view. Runtime `room.message` and `room.say` return only an accepted message-ID receipt. Host management routes retain their full operator results. Policy-administration commands can delegate more privileges; `room.message` and `room.say` authorize room/role authorship. Assign these permissions deliberately. Command results may include invite material; do not relay them into chat.
36
72
 
37
73
  Host lifecycle and global room creation/listing are excluded. `room.accept` accepts invitation input through its separate grant and remains unavailable through REST. Ours close/delete return a durable accepted receipt before closing the reply channel; verify completion through management. Close retains archive/files; delete requires `confirm:true`, closes first, and erases local room data. Pending lifecycle requests resume after restart; failed requests remain visible in room metadata for explicit management retry. Missing replies do not prove a mutation failed.
38
74
 
@@ -37,6 +37,10 @@ ours-cowork room history <room-id>
37
37
  ours-cowork room history <room-id> --after 40 --limit 20
38
38
  ```
39
39
 
40
+ The host management API, web archive, and default CLI history retain complete records and global archive sequence numbers, including scoped thread records and intake rejection results. The host-only `--view participant` option remains a legacy alias-redacted message view with global cursors; it has no viewer identity and may include every thread's messages. Its explicit allowlist omits internal thread scope, retry keys, source references, and recipient identities.
41
+
42
+ Runtime `room.history` is a separate authenticated participant API. It returns messages only, hides private roots and descendants from excluded or removed/re-added seats, and exposes only public thread metadata to eligible selected seats. Its `after` cursor and record IDs count that viewer's visible messages, so hidden activity creates no gaps or counts. The default limit is 200. Continue from the last returned `seq`; `view` may only be `participant`, and callers cannot choose another viewer. Old runtime callers must discard global archive cursors when switching to this API. Host archive cursors and participant cursors are not interchangeable.
43
+
40
44
  Daemon history responses are capped at 3 MiB of JSON so one 2 MiB file record (about 2.8 MiB after base64 expansion) remains retrievable without allowing an unbounded management response. A page may therefore contain fewer records than `--limit` even when more records exist. Continue from the last returned `seq` with `--after`; only an empty page means end of history. The CLI follows these byte-short pages automatically and still prints up to the requested record limit.
41
45
 
42
46
  Participant messages and files are accepted only from durable seats in an active room. A file is opaque binary data: cowork neither interprets its MIME metadata nor executes its contents. Its filename must be a path-free name of at most 255 UTF-8 bytes (not `.`, `..`, or a name containing `/`, `\\`, or NUL); MIME metadata may be empty and is limited to 255 UTF-8 bytes. Zero-byte files are valid. The maximum file size is 2 MiB (2,097,152 bytes), and a larger file is rejected explicitly.
@@ -58,11 +62,13 @@ Cowork history records include messages, files, and the durable relay intent/res
58
62
 
59
63
  ## Reply threading
60
64
 
61
- A reply is broadcast to the other active room participants, with no sender echo. The parent author receives a reference to their original source message or file; other participants receive a reference to their own recorded room copy. Multiple recorded copies remain aliases of one logical original. For files, both the metadata notice and binary copy can identify that original.
65
+ An ordinary reply is broadcast to the other active room participants, with no sender echo. The parent author receives a reference to their original source message or file; other participants receive a reference to their own recorded room copy. Multiple recorded copies remain aliases of one logical original. For files, both the metadata notice and binary copy can identify that original.
66
+
67
+ Replies to a `start_thread` root or descendant use its immutable selected audience instead. Cowork accepts them only from an original selected seat that is still active, then relays them only to other original selected seats that are still active. Creating the thread requires the `start_thread` command grant; later native replies do not. A message with no reply target remains an ordinary whole-room message and is never implicitly attached to a scoped thread. Scoped file replies are rejected in version one.
62
68
 
63
- Cowork derives these relationships from its existing archive. If a parent or a recipient's copy cannot be resolved, it delivers the answer without a transport parent and retains the incoming source reference. It does not replay history or scan SDK history to repair links. Queued records do not guarantee that a recipient still has the parent, and a wire lost before result persistence can remain unresolved. Existing retry behavior can create distinct transport copies.
69
+ Cowork derives reply relationships from its existing archive. For an ordinary reply, an unavailable recipient copy may still produce an unlinked delivery under the existing best-effort behavior. A scoped reply is stricter: an invalid, missing, expired, ambiguous, or unauthorized target fails closed instead of becoming a broadcast, and a selected recipient whose local parent copy cannot be proven is skipped terminally instead of receiving unlinked private content. Cowork does not replay history or scan SDK history to repair links. Queued records do not guarantee that a recipient still has the parent, and a wire lost before result persistence can remain unresolved. Existing retry behavior can create distinct transport copies.
64
70
 
65
- Reply translation changes transport metadata only. Room JSON bodies and version-one archive formats remain unchanged; incoming sentence references are retained but translated outgoing references use only the selected wire ID. Existing history views retain their logical-record/audit separation; reading history does not send messages. Ordinary clients can use their existing native reply handling without implementing Cowork mapping logic.
71
+ Reply translation changes transport metadata only. Incoming sentence references are retained, but translated outgoing references use only the selected wire ID. Participant-facing scoped envelopes add public `thread` metadata and roots add public topic, creator, participant IDs, and creation time; retry keys, real anonymous identities, recipient identities, and routing references stay host-only. Existing history views retain their logical-record/audit separation; reading history does not send messages. Ordinary clients can use their existing native reply handling without implementing Cowork mapping logic.
66
72
 
67
73
  The web console projects participant and room-authored messages plus the briefing into Communication. Relay, file, recovery, close, and failure records are excluded from chat and shown in Events; Archive retains the complete ordered record stream, including archived file bytes. Messages appear only after the authoritative history refresh observes them.
68
74
 
@@ -4,7 +4,7 @@ Stop the cowork daemon before taking a cowork backup. A live copy can split room
4
4
 
5
5
  Back up the complete cowork state directory as one unit, preserving ownership and file modes. Do not select only individual room JSON or archive files. Shared ours identity state is outside this directory and must be protected separately through the shared daemon's own stopped-state backup procedure. That separate state includes each room identity's `history.sqlite3` and immutable content-addressed blobs; copying only the cowork archive does not back up contacts, invites, unread state, or SDK history.
6
6
 
7
- For restore, stop cowork, replace the complete cowork state directory with the complete backup, restore its original owner and `0700`/`0600` permissions, ensure the shared daemon already contains the corresponding room identities, and then start cowork. Do not merge individual room directories from different snapshots. Restore to a compatible package version and verify `ours-cowork status` plus representative `room show` and `room history` calls.
7
+ For restore, stop cowork, replace the complete cowork state directory with the complete backup, restore its original owner and `0700`/`0600` permissions, ensure the shared daemon already contains the corresponding room identities, and then start cowork. Do not merge individual room directories from different snapshots. Restore to a compatible package version and verify `ours-cowork status` plus representative `room show` and `room history` calls. Opening a version-one archive migrates it transactionally to version two for scoped-thread indexes. After that migration, do not run an older Cowork release against the archive: the old reader refuses version two. Downgrade requires an explicit, compatible restore of the complete Cowork and shared-daemon state from before migration; do not erase thread scope fields or rewrite the archive in place.
8
8
 
9
9
  An established room restores only by choosing its exact persisted `ours-cowork:<bounded creation name>` identity with `force: false` and verifying the exact durable identity CID. Mutable `room_name` metadata does not participate in established restore. If that exact identity is absent from the shared daemon or its CID differs, startup fails clearly; cowork never renames, adopts, or recreates an established identity. Only a durable `packet_pending` room may create a missing identity. Upgrade recovery may rewrite an empty-CID overlength sentinel to the deterministic 64-code-point bounded name before that first creation; it never rewrites a name with an established CID.
10
10
 
@@ -11,6 +11,7 @@
11
11
  - Backups require a stopped cowork daemon. Back up and restore the complete cowork state directory as one unit, preserving ownership and modes; protect shared identity state separately through the shared daemon's operator procedure.
12
12
  - Service uninstall retains data. It removes the cowork systemd or launchd definition, not configuration, archives, room metadata, or identities in the shared daemon.
13
13
  - Closing retains room metadata, archive and files. `ours-cowork room delete <room-id> --yes` closes the room if necessary, then erases all local room data. Ours close/delete replies acknowledge durable acceptance; management state determines completion.
14
+ - Scoped roots, replies, rejections, and relay results follow that same room archive lifecycle. Cowork has no separate thread-body retention window: close retains them and confirmed room deletion removes their local records. The selected participants' SDK-local message history is a separate store with its own retention behavior and does not change Cowork's room routing or archive retention.
14
15
  - Confirmed deletion removes the retained archive and metadata from this host only. It does not claim remote purge, backup erasure, key wipe, or secure erase.
15
16
  - The web console and HTTP room RPC have no authentication. They bind only to `127.0.0.1` and must not be forwarded, proxied, or exposed remotely.
16
17
  - Web updates use periodic polling rather than push. A view can lag daemon state until its next refresh; confirmed mutations trigger an immediate refresh.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/cowork",
3
- "version": "1.1.4",
3
+ "version": "1.2.0",
4
4
  "description": "Durable ours mission rooms on the shared ours daemon.",
5
5
  "type": "module",
6
6
  "license": "FSL-1.1-Apache-2.0",