Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ca562c0
notice contact messages in the ship chat
jess-cat Oct 7, 2026
e277271
document the ship chat contact notice
jess-cat Oct 7, 2026
2fb9f81
square the reply box in the contact notice
jess-cat Oct 7, 2026
3a2eb4c
reply to a contact notice inline and keep it after answering
jess-cat Oct 7, 2026
3564d03
preview the newest contact message on its notice
jess-cat Oct 7, 2026
90d3133
keep failed and uncertain notice replies open, carry attachments, ret…
jess-cat Oct 7, 2026
df4f5bd
pin a reply retry to its submitted message and listen only while the …
jess-cat Oct 7, 2026
a488f9e
show notices on an empty ship chat and carry an unsent reply to the chat
jess-cat Oct 7, 2026
856328b
keep an unsent notice reply with its notice instead of handing it to …
jess-cat Oct 7, 2026
0517ad4
own ship notices above the keyed chat view and listen from the first …
jess-cat Oct 7, 2026
4868b06
recheck a held notice once its draft goes
jess-cat Oct 7, 2026
ad5031d
guard notice reply drafts as unsaved work while a helper is shown
jess-cat Oct 7, 2026
b234ce2
merge contact notices into social onboarding
deathbyknowledge Oct 8, 2026
40933ad
add contact invitation links and unread selection
deathbyknowledge Oct 8, 2026
b4dba9a
build the first connection experience
deathbyknowledge Oct 8, 2026
37a1d19
support cross-operator invitation links
deathbyknowledge Oct 8, 2026
42db07a
keep people within reach in zen
deathbyknowledge Oct 9, 2026
c0d1ab4
choose contact handling when connecting
deathbyknowledge Oct 9, 2026
fb1c0f1
align empty contact conversations with messages
deathbyknowledge Oct 9, 2026
cafa198
show reply on the first contact message
deathbyknowledge Oct 9, 2026
2a5af40
support ship-led contact handling choices
deathbyknowledge Oct 9, 2026
7e873de
share contact conversation state
deathbyknowledge Oct 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/architecture/conversations.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,20 @@ owner and are resolved only when a user inspects a span.

## Storage and retention

Instrument's People indicator and Zen activity strip read the ordinary owner-scoped
inbox with `attentionOnly: true` and the incoming request list. The Kernel filters
unread, active, unmuted, unblocked conversations before pagination; the caller selects
the usual archived state. Existing `conversation.changed`, `contact.changed` and
`approach.changed` signals invalidate those bounded reads, and reconnect refreshes
them. Attention survives reload through the private inbox projection. Zen and People
observe the same paginated conversation cache; Zen has no separate live-message or
replied-notice store. Only unfinished reply drafts and their submitted send identities
remain local. A draft retains the read position where it began, so reads elsewhere do
not hide its context or pull older read history into the panel. Each send retains the
exact sequence it answers, so a later arrival stays unread when that reply completes.
A failed read update leaves the inbox authoritative
and surfaces the error without repeating the successful send.

The Kernel Durable Object stores the conversation directory, membership, optional handler, surface
mapping, latest sequence and private inbox projection. Contact previews, read positions and archive
state stay in that projection; canonical messages have one owner. Each conversation has its own installation-scoped Conversation Durable
Expand Down
7 changes: 7 additions & 0 deletions docs/get-started/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,13 @@ The web console is called **Instrument** and has five views:

The Settings sidebar highlights the section you're viewing.

Open **People** to try making plans or working together with someone on GSV.
**connect** creates a shareable invitation; accepting it opens your conversation.
You can talk directly or choose **ask Ship** to prepare a request in your Ship chat.
A dot beside **people** marks waiting activity, and Zen shows unread conversations
and message requests above the prompt, including after a reload. See
[Contact people](/how-to/contact-people) for the full flow.

If your operator enables cloud browsers, Fleet also offers **start browser**.
You can [sign in and save website logins](/how-to/cloud-browsers) there so Ship
can use those sites while your personal devices are offline.
Expand Down
77 changes: 63 additions & 14 deletions docs/how-to/contact-people.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,37 @@ Search inside a conversation with **search** or `Ctrl/Cmd+F`. Open **details** f
the private name, mute and connection controls. **Save contact** adds someone to Contacts without
changing their permissions or how Ship handles the conversation.

## Start a conversation
On your first visit, try an example: make plans, plan a trip, or work together.
Choose **try this with someone** to connect. The example remains available when
your conversation opens, so you can turn it into a request to Ship.

Choose **new** to find a saved contact by name or enter someone's public profile address. Review
the profile, choose the display name they will see, and send a first message.
They can accept or decline it. Acceptance keeps that first message in the same
conversation and enables further messages and attachments.
## Start a conversation

Alternatively, choose **use a private invitation** to create or accept a one-use
contact code. These invitations are separate from
Choose **connect**, choose who should handle new messages, then **create invitation
link** and share it wherever you already talk. It connects one person and lasts seven days; you can cancel it before it is
accepted. In managed spaces, the link opens the existing Accounts space chooser.
The recipient chooses a listed space or enters its address, signs in if needed,
and chooses who should handle their new messages before accepting the invitation. Entering an address also works for people
using another operator or a space they do not own. Other deployments open the
space-address form directly.
Neither person needs to publish a profile. Acceptance opens the conversation.

If someone gives you a link or an older contact code, choose **connect → I have an
invitation** to paste it. These invitations are separate from
[inviting an account into your space](/how-to/invite-people).

You can also ask Ship to create an invitation, or give it someone's invitation
link to accept. Ship asks who should handle new messages unless you already told
it, then completes the connection with that choice. It can also change the choice
later when you ask. This works in your ordinary Ship conversation, including
messaging apps; opening People is optional.

Choose **connect → use a profile address** to find a saved contact by name or enter
someone's public profile address. Review the profile and your prefilled display
name, choose who handles new messages, then send a first message.
They make their own handling choice when accepting, or can decline it. Acceptance keeps that first message in the same
conversation, opens it directly, and enables further messages and attachments.

Your profile starts private. In **Settings → Profile**, save a draft, review it
and explicitly publish it. A profile can accept message requests, require a private
invitation, or close new contact. Saving later edits does not change the public
Expand All @@ -28,11 +48,38 @@ conversations remain. People who already viewed it may retain copies.

## Decide who handles it

Accepting a contact does not start Ship. Enable **Ship replies** beside the person's
name to let Ship respond to new incoming messages using its ordinary permissions
and approval rules. Turning it on waits for the next message; it does not start work
on the existing conversation. Turn it off to stop ongoing handling. Messages
distinguish the person from their Ship.
When connecting in People, both people explicitly choose **I’ll handle them** or
**Let Ship handle them** for their own side. Neither option is preselected. The
choice stays with the invitation or request, even if acceptance happens later.
Existing contacts keep their settings. Ship-led invitations use the same explicit
choice; creating or accepting one does not silently choose automatic handling.

**Automatically handle new messages**, beside the person's name, changes this
choice. When on, Ship can respond using its ordinary permissions and approval
rules. Accepting or enabling the setting does not wake Ship or replay existing
messages; the next incoming message starts handling. Turning it off ends that
standing assignment. Replies to tasks you assign separately can still resume
those tasks until they finish. Messages distinguish the person from their Ship.

For a particular task, choose **ask Ship** in the conversation. This opens an
editable request in your ordinary Ship chat. Review it and send it when ready.
It does not turn on automatic replies for that person. A first-use example can
prefill a more specific request, such as finding a time for dinner.

The **people** line above the Ship prompt keeps unread conversations and incoming
requests within reach, including after reload or reconnect. New messages update
the line without opening a panel or moving your place in the Ship conversation.
Select a person's name to read and reply in a compact panel. A **PERSON** or
**GSV** badge identifies who wrote the latest message. **Open conversation** takes
you to the full history in People; selecting a connection request opens it there.
Conversation previews label outgoing messages **You** or **Your Ship**, so they
cannot be mistaken for a new reply from the other person.

Closing the panel keeps an unfinished reply for this session, marked **draft** on
the person's name. Sending threads the reply to the message it answers and clears
that answered activity; a newer arrival still waits. A dot beside **people** in
the main navigation also marks unread activity. Muted, blocked, ended and archived
conversations stay quiet; an unfinished draft remains reachable.

Work for a particular task is separate from that standing preference. When Ship
sends with `message send --to contact:ID --responsibility R12Y_ID`, replies continue
Expand All @@ -43,15 +90,17 @@ ends that association. The receiving person independently chooses who handles
their side.

A message’s **reply** action quotes that message in the same conversation and keeps
its exact reference. Reply and successful delivery information appear on hover or
its exact reference, including on the first incoming message before you have sent
anything back. Reply and successful delivery information appear on hover or
keyboard focus; touch screens keep the actions visible. Delivery problems remain
visible until resolved.

## Private controls

Read position, archive, saved contacts, aliases and mute are private to your space.
New messages bring an archived conversation back unless muted. Muting also suppresses
tab attention; it does not revoke Ship's existing assignment.
tab attention and the Ship chat's new-message line; it does not revoke Ship's existing
assignment.

Blocking ends the connection, withdraws its attachment grants and refuses new
messages and requests from that identity. Unblocking permits a new connection;
Expand Down
5 changes: 5 additions & 0 deletions docs/how-to/install-host-apps.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ to use text explicitly.

## Sign-in and multiple windows

**Open your space** lets you choose an owned space or enter a handle or domain.
The address form also works for spaces owned by someone else or served by another
operator. Contact invitation links use the same address entry before your normal
space sign-in and explicit acceptance.

Desktop and browser windows can stay connected to the same space at once. They
receive new conversation messages live and refresh server state after a
reconnect. A remembered space sign-in lasts 30 days and renews during use.
Expand Down
20 changes: 15 additions & 5 deletions docs/reference/cli-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,11 @@ message send --to DESTINATION [--message TEXT] [--attach PATH]... [--mime TYPE]
contact identity
contact list [--all] [--json]
contact alias CONTACT_ID NAME|--clear
contact invite create [--expires DURATION]
contact invite accept CODE
contact invite create --handling manual|ship [--expires DURATION]
contact invite accept LINK_OR_CODE --handling manual|ship
contact invite list [--all] [--json]
contact invite cancel INVITE_ID
contact handling CONTACT_ID manual|ship --revision N
contact revoke CONTACT_ID
contact request list [--contact CONTACT_ID] [--all] [--json]
contact request create --contact CONTACT_ID --kind KIND --title TITLE [--details JSON] [--delivery-id ID]
Expand Down Expand Up @@ -313,13 +314,22 @@ automatic retry. An outcome that may have reached the provider is reported as
`sent=false`, `delivery_confirmed=false`, and `delivery_state=ambiguous`.

`contact` manages relationships with people on other GSV installations.
`contact invite create` produces a short-lived one-use code; the other person
accepts that code while signed in to their own GSV. Pairing and revocation may
`contact invite create --handling manual|ship` returns a short-lived one-use code and a shareable `url`;
the other person accepts either while signed in to their own GSV. The default
lifetime is one hour; `--expires` accepts up to seven days. Pairing and revocation may
be performed by the signed-in human or their canonical Ship. `contact list` prints the opaque contact id accepted
by `message send --to`; `message destinations` exposes the same active contacts
alongside messaging endpoints. `contact alias` changes only the local display
name; the remote Ship's authenticated identity remains visible and unchanged.

Creating and accepting through Shell require `--handling manual|ship`. Ship asks
for the owner's choice unless it was already given: `manual` leaves new messages
for the person, while `ship` enables automatic handling. Each side chooses
independently. To change an existing contact, read `preferences.revision` from
`contact list --json` and use `contact handling CONTACT_ID manual|ship --revision N`.
The Kernel accepts changes from the owner or their canonical Ship, keeps capability
and revision checks, and starts no work merely because the setting changed.

`contact invite list --all` exposes retained invitation lifecycle metadata but
never a recoverable code. `message history --with contact:...` reads the Contact
conversation. A Contact send reports durable local acceptance separately from
Expand All @@ -328,7 +338,7 @@ remote confirmation; use `message delivery show` with its delivery id.
When Ship contacts someone for an existing task, pass `--responsibility ID` to
associate replies with that open Ship responsibility. A reply continues the same
work without enabling permanent handling of that contact. Acceptance and new
messages stay in People unless the person chooses **Let Ship handle this**.
messages stay in People unless the person chooses **Let Ship handle them**.

Use `contact request create` and `contact request update` when the exchange has
a durable lifecycle rather than being only a message. Request revisions prevent
Expand Down
27 changes: 20 additions & 7 deletions docs/reference/syscalls.md
Original file line number Diff line number Diff line change
Expand Up @@ -696,7 +696,7 @@ shared account system. Each installation keeps its own conversation, request,
resource grants, delivery receipts, and Process state.

Pairing is explicit and human-controlled. `contact.invite.create` returns a
short-lived, one-use code; the other signed-in person accepts it with
short-lived, one-use code and a shareable `url`; the other signed-in person accepts either with
`contact.invite.accept`. The Kernel derives the remote installation and subject
from the signed exchange. Callers never choose a remote local uid, Process,
conversation, filesystem path, or installation id.
Expand All @@ -705,16 +705,27 @@ Trust changes may be initiated by the signed-in human or by that owner's exact
canonical Ship Process. Delegated work Processes and remote callers cannot
create, accept, cancel, or revoke Contact trust.

`contact.invite.create` and `contact.invite.accept` accept an optional
`shipHandlesMessages` boolean for the local owner's new contact. Setting it
requires the signed-in owner or their canonical Ship with `contact.preferences.update`.
Ship must explicitly supply the owner's choice; omission is rejected for Processes.
The Kernel retains it with the invitation or acceptance attempt and applies it when
that contact generation activates. Older human clients may omit it to keep automatic
handling off. Replaying a completed acceptance does not overwrite later
preference changes. `approach.create` and an `approach.decide` acceptance support
the same local choice, retained through asynchronous pairing and retries. It is
never supplied by the remote person, and activation admits no Ship work itself.

| Syscall | Behavior |
|---|---|
| `contact.identity` | Returns this installation's signed Ship document and the caller's local federation subject. |
| `contact.invite.create` | Creates a one-use pairing code, optionally with a shorter expiry. |
| `contact.invite.accept` | Verifies and consumes a remote invite, creates both contact records, and ensures the local Contact conversation. |
| `contact.invite.create` | Creates a one-use pairing code and shareable link, with an optional expiry of up to seven days. |
| `contact.invite.accept` | Accepts a link or code, verifies and consumes the remote invite, creates both contact records, and ensures the local Contact conversation. |
| `contact.invite.list` | Lists invitation lifecycle metadata without exposing recoverable invitation secrets. |
| `contact.invite.cancel` | Cancels one unaccepted invitation. |
| `contact.list` | Lists the caller's active contacts; `includeRevoked` includes terminal relationships. |
| `contact.alias.set` | Sets or clears the owner's local name for a Contact without changing or federating its authenticated remote identity. |
| `contact.preferences.update` | Human-only, revision-checked changes to saved, muted and standing Ship handling preferences. |
| `contact.preferences.update` | Owner or canonical Ship, revision-checked changes to saved, muted and standing Ship handling preferences. |
| `contact.block.set` | Human-only block or unblock of one remote actor; blocking also ends its active connection and pending first-contact requests. |
| `contact.block.list` | Reads private blocks, optionally filtered by `actor`, with cursor paging. |
| `contact.notice.dismiss` | Dismisses the one-time notice that global contact auto-wake has been retired. |
Expand Down Expand Up @@ -823,7 +834,7 @@ type ContactSyscalls = {
};
"contact.invite.create": {
args: { expiresInSeconds?: number };
result: { inviteId: string; code: string; expiresAtMs: number };
result: { inviteId: string; code: string; url?: string; expiresAtMs: number };
};
"contact.invite.accept": {
args: { code: string };
Expand Down Expand Up @@ -1030,7 +1041,7 @@ actor independently of whether a contact was ever accepted. Unblocking does not
```ts
type ConversationInboxSyscalls = {
"conversation.inbox": {
args: { archived?: boolean; before?: { updatedAt: number; conversationId: string }; limit?: number };
args: { archived?: boolean; attentionOnly?: boolean; before?: { updatedAt: number; conversationId: string }; limit?: number };
result: { entries: ConversationInboxEntry[]; next?: { updatedAt: number; conversationId: string } };
};
"conversation.view.get": {
Expand All @@ -1045,7 +1056,9 @@ type ConversationInboxSyscalls = {
```

`conversation.inbox` lists accepted contact conversations, optionally filtered by `archived`, with
`before` and `limit` paging. Entries contain the contact ID, conversation, latest preview, unread
`before` and `limit` paging. `attentionOnly: true` selects unread conversations with active,
unmuted, unblocked contacts before pagination. This uses the same owner-private read state
as the ordinary inbox. Entries contain the contact ID, conversation, latest preview, unread
state and private view state. `conversation.view.get` reads one entry; `conversation.view.update`
advances `readThroughSequence` monotonically or sets `archived`. Both use `conversationId`.
Changing `archived` requires `expectedRevision` from the current view to avoid overwriting a newer change.
Expand Down
Loading
Loading