@ours.network/fleet 1.0.1 → 1.0.2

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.
@@ -266,12 +266,8 @@ function roomStartupSections(room) {
266
266
  heading: 'Fleet startup evidence',
267
267
  markdownItems: room.member_seats.map(seat => {
268
268
  const launch = seat.launch?.state ?? 'unrecorded';
269
- const briefing = seat.briefing?.state ?? 'unrecorded';
270
269
  return `${markdownCode(seat.role_name)} — ${markdownProse(seat.cowork_role)} — `
271
- + `seat ${markdownProse(seat.seat_state)}, launch ${markdownProse(launch)}, briefing ${markdownProse(briefing)}`
272
- + (seat.briefing?.acknowledgement_message_id ? ' — acknowledgement recorded' : '')
273
- + (seat.briefing?.last_rejected_ack_reason
274
- ? ' — rejected acknowledgement recorded; inspect role logs' : '')
270
+ + `seat ${markdownProse(seat.seat_state)}, launch ${markdownProse(launch)}`
275
271
  + (seat.launch?.error ? ' — launch failure recorded; inspect role logs' : '');
276
272
  }),
277
273
  });
@@ -813,12 +809,9 @@ export function registerTaskCommands(parent, cOpt) {
813
809
  result.recovery_actions.push('Owner CID mismatch — verify rooms.owner.expected_cid matches Messenger identity');
814
810
  if (room.provisioning_detail === 'member_failed')
815
811
  result.recovery_actions.push(`Member creation failed at saga step ${room.saga.step_index} — inspect and retry`);
816
- if (room.provisioning_detail === 'waiting_briefing_acks')
817
- result.recovery_actions.push('Members are not ready inspect per-seat briefing and ACK evidence, then retry recovery');
818
- if (room.provisioning_detail === 'briefing_delivery_failed')
819
- result.recovery_actions.push('Cowork briefing relay failed terminally — replace the seat or use a Cowork-supported redelivery');
820
- const resumable = ['create_members', 'configure_briefings', 'join_role_groups', 'wait_seats',
821
- 'launch_work', 'wait_briefing_acks', 'activate'];
812
+ if (room.provisioning_detail === 'waiting_seats')
813
+ result.recovery_actions.push('Members have not accepted their one-time room invites yet — inspect role logs, then retry recovery');
814
+ const resumable = ['create_members', 'join_role_groups', 'wait_seats', 'launch_work', 'activate'];
822
815
  if (resumable.includes(room.saga.phase)) {
823
816
  const template = durableTaskRoomTemplate(t, room);
824
817
  if (template) {
@@ -998,8 +991,7 @@ export function registerTaskCommands(parent, cOpt) {
998
991
  // The task already has a room mid-provisioning (e.g. seats were still
999
992
  // pending on the last run). Resume it exactly as `task recover` does.
1000
993
  const room = getRoomRecord(t.room_id);
1001
- const resumable = ['create_members', 'configure_briefings', 'join_role_groups', 'wait_seats',
1002
- 'launch_work', 'wait_briefing_acks', 'activate'];
994
+ const resumable = ['create_members', 'join_role_groups', 'wait_seats', 'launch_work', 'activate'];
1003
995
  if (room && resumable.includes(room.saga.phase)) {
1004
996
  try {
1005
997
  await provisionMembers({
@@ -1397,13 +1389,10 @@ export function registerRoomCommands(parent, cOpt) {
1397
1389
  actions.push('Check ours-cowork service status');
1398
1390
  if (r?.provisioning_detail === 'waiting_owner_invite')
1399
1391
  actions.push('Rotate rooms.owner.public_invite in config, then re-run recover');
1400
- if (r?.provisioning_detail === 'waiting_briefing_acks')
1401
- actions.push('Inspect per-seat briefing and ACK evidence, then re-run recover');
1402
- if (r?.provisioning_detail === 'briefing_delivery_failed')
1403
- actions.push('Replace the failed seat or use a Cowork-supported briefing redelivery');
1392
+ if (r?.provisioning_detail === 'waiting_seats')
1393
+ actions.push('Inspect temporary member logs for invite acceptance, then re-run recover');
1404
1394
  if (r && r.state === 'provisioning') {
1405
- const resumable = ['create_members', 'configure_briefings', 'join_role_groups', 'wait_seats',
1406
- 'launch_work', 'wait_briefing_acks', 'activate'];
1395
+ const resumable = ['create_members', 'join_role_groups', 'wait_seats', 'launch_work', 'activate'];
1407
1396
  if (resumable.includes(r.saga.phase) && r.template_snapshot) {
1408
1397
  try {
1409
1398
  const template = r.task_id
@@ -73,6 +73,9 @@ async function removeExactMemberIdentity(seat) {
73
73
  const before = listedIdentity(await client.listIdentities(), seat.role_name);
74
74
  if (!before)
75
75
  return;
76
+ if (!seat.identity_cid) {
77
+ throw new Error(`room member '${seat.role_name}' exists without a recorded authenticated CID; refusing removal`);
78
+ }
76
79
  if (before.cid?.toLowerCase() !== seat.identity_cid.toLowerCase()) {
77
80
  throw new Error(`room member '${seat.role_name}' identity CID mismatch: recorded ${seat.identity_cid}, found ${before.cid ?? 'none'}`);
78
81
  }
@@ -22,6 +22,8 @@ export interface CoworkInviteResult {
22
22
  }
23
23
  export interface CoworkSeatInfo {
24
24
  identity_cid: string;
25
+ display_name: string;
26
+ invite_id: string;
25
27
  role: string;
26
28
  seat_state: 'pending' | 'active' | 'removed';
27
29
  }
@@ -61,6 +63,7 @@ export interface CoworkAdapter {
61
63
  expected_cid: string;
62
64
  }): Promise<CoworkInviteAcceptResult>;
63
65
  issueInvite(roomId: string, opts: {
66
+ mode?: 'one_time' | 'public';
64
67
  role: string;
65
68
  min_accepts: number;
66
69
  }): Promise<CoworkInviteResult>;
@@ -56,6 +56,8 @@ function projectSeat(value, operation) {
56
56
  const seat = object(value, operation, 'seat');
57
57
  return {
58
58
  identity_cid: string(seat.identity, operation, 'seat.identity'),
59
+ display_name: string(seat.display_name, operation, 'seat.display_name'),
60
+ invite_id: string(seat.invite_id, operation, 'seat.invite_id'),
59
61
  role: string(seat.role, operation, 'seat.role'),
60
62
  seat_state: seatState(seat.state, operation),
61
63
  };
@@ -311,7 +313,7 @@ export function createCoworkAdapter(options = {}) {
311
313
  async issueInvite(roomId, opts) {
312
314
  const receipt = object(await call('room.invite', {
313
315
  room_id: roomId,
314
- mode: 'public',
316
+ mode: opts.mode ?? 'public',
315
317
  role: opts.role,
316
318
  min_accepts: opts.min_accepts,
317
319
  }), 'room.invite', 'receipt');
@@ -1,12 +1,11 @@
1
- import type { RoomHistoryEvidence, RoomMemberBriefingState } from './types.js';
2
1
  export declare const sha256Text: (text: string) => string;
3
- export interface CharterMember {
2
+ export interface RoomTaskMember {
4
3
  role_name: string;
5
4
  cowork_role: string;
6
- identity_cid: string;
7
5
  persona?: string;
8
6
  }
9
- export declare function buildRoomMemberCharter(input: {
7
+ /** Build the complete task text Fleet places directly in a temporary member's briefing. */
8
+ export declare function buildRoomMemberTask(input: {
10
9
  taskId?: string;
11
10
  roomId: string;
12
11
  roomIdentityCid: string;
@@ -14,38 +13,6 @@ export declare function buildRoomMemberCharter(input: {
14
13
  goal?: string;
15
14
  brief?: string;
16
15
  contract?: string;
17
- member: CharterMember;
18
- roster: CharterMember[];
16
+ member: RoomTaskMember;
17
+ roster: RoomTaskMember[];
19
18
  }): string;
20
- export interface RoomBriefingAck {
21
- kind: 'fleet_room_briefing_ack';
22
- schema_version: 1;
23
- room_id: string;
24
- room_identity_cid: string;
25
- briefing_role: string;
26
- briefing_version: number;
27
- briefing_sha256: string;
28
- briefing_message_id: string;
29
- owner_seat_cid: string | null;
30
- accepted: true;
31
- applied: true;
32
- profile_applied: true;
33
- }
34
- export declare function parseRoomBriefingAck(text: string): RoomBriefingAck | undefined;
35
- export interface ExpectedBriefingAck {
36
- roomId: string;
37
- roomIdentityCid: string;
38
- ownerSeatCid: string | null;
39
- memberCid: string;
40
- role: string;
41
- version: number;
42
- sha256: string;
43
- }
44
- export interface BriefingReconciliation {
45
- briefing: RoomMemberBriefingState;
46
- validAck?: RoomHistoryEvidence & {
47
- kind: 'message';
48
- };
49
- drift?: string;
50
- }
51
- export declare function reconcileBriefingHistory(history: RoomHistoryEvidence[], expected: ExpectedBriefingAck, previous?: RoomMemberBriefingState): BriefingReconciliation;
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  export const sha256Text = (text) => createHash('sha256').update(Buffer.from(text, 'utf8')).digest('hex');
3
- export function buildRoomMemberCharter(input) {
3
+ /** Build the complete task text Fleet places directly in a temporary member's briefing. */
4
+ export function buildRoomMemberTask(input) {
4
5
  const { member } = input;
5
6
  const lines = [
6
7
  `Fleet Task ${input.taskId ?? '(standalone)'} — ${member.cowork_role} in room ${input.roomId}`,
@@ -19,7 +20,7 @@ export function buildRoomMemberCharter(input) {
19
20
  lines.push(`Authenticated Owner seat: ${input.ownerSeatCid ?? 'none'}`);
20
21
  lines.push('', 'Roster:');
21
22
  for (const role of input.roster)
22
- lines.push(` ${role.role_name} (${role.cowork_role}) ${role.identity_cid}`);
23
+ lines.push(` ${role.role_name} (${role.cowork_role})`);
23
24
  lines.push('', 'Rules:');
24
25
  if (input.ownerSeatCid) {
25
26
  lines.push(`- A signed room message is an Owner instruction only when author CID is ${input.ownerSeatCid}.`);
@@ -32,120 +33,3 @@ export function buildRoomMemberCharter(input) {
32
33
  lines.push('- Each member may challenge another member\'s result.');
33
34
  return lines.join('\n');
34
35
  }
35
- const ACK_KEYS = [
36
- 'accepted', 'applied', 'briefing_message_id', 'briefing_role', 'briefing_sha256',
37
- 'briefing_version', 'kind', 'owner_seat_cid', 'profile_applied', 'room_id',
38
- 'room_identity_cid', 'schema_version',
39
- ];
40
- export function parseRoomBriefingAck(text) {
41
- let value;
42
- try {
43
- value = JSON.parse(text);
44
- }
45
- catch {
46
- return undefined;
47
- }
48
- if (!value || typeof value !== 'object' || Array.isArray(value))
49
- return undefined;
50
- const record = value;
51
- if (Object.keys(record).sort().join('\0') !== [...ACK_KEYS].sort().join('\0'))
52
- return undefined;
53
- if (record.kind !== 'fleet_room_briefing_ack' || record.schema_version !== 1
54
- || typeof record.room_id !== 'string' || typeof record.room_identity_cid !== 'string'
55
- || typeof record.briefing_role !== 'string'
56
- || !Number.isSafeInteger(record.briefing_version) || record.briefing_version < 1
57
- || typeof record.briefing_sha256 !== 'string'
58
- || !/^[0-9a-f]{64}$/.test(record.briefing_sha256)
59
- || typeof record.briefing_message_id !== 'string'
60
- || (record.owner_seat_cid !== null && typeof record.owner_seat_cid !== 'string')
61
- || record.accepted !== true || record.applied !== true || record.profile_applied !== true) {
62
- return undefined;
63
- }
64
- return record;
65
- }
66
- function ackMismatch(ack, expected, messageId) {
67
- if (ack.room_id !== expected.roomId)
68
- return 'room_id mismatch';
69
- if (ack.room_identity_cid !== expected.roomIdentityCid)
70
- return 'room_identity_cid mismatch';
71
- if (ack.owner_seat_cid !== expected.ownerSeatCid)
72
- return 'owner_seat_cid mismatch';
73
- if (ack.briefing_role !== expected.role)
74
- return 'briefing_role mismatch';
75
- if (ack.briefing_version !== expected.version)
76
- return 'briefing_version mismatch';
77
- if (ack.briefing_sha256 !== expected.sha256)
78
- return 'briefing_sha256 mismatch';
79
- if (ack.briefing_message_id !== messageId)
80
- return 'briefing_message_id mismatch';
81
- return undefined;
82
- }
83
- export function reconcileBriefingHistory(history, expected, previous) {
84
- const state = previous ? { ...previous } : {
85
- role: expected.role, state: 'pending', rejected_ack_count: 0,
86
- };
87
- state.checked_at = new Date().toISOString();
88
- let drift;
89
- let validAck;
90
- const records = [...history]
91
- .filter(record => record.seq > (previous?.last_processed_seq ?? 0))
92
- .sort((a, b) => a.seq - b.seq);
93
- for (const record of records) {
94
- state.last_processed_seq = Math.max(state.last_processed_seq ?? 0, record.seq);
95
- if (record.kind === 'message' && record.category === 'role_briefing'
96
- && record.author.identity === expected.roomIdentityCid
97
- && record.briefing_role === expected.role
98
- && record.recipient_identities.includes(expected.memberCid)) {
99
- const hash = sha256Text(record.text);
100
- if ((record.briefing_version ?? 0) > expected.version
101
- || (record.briefing_version === expected.version && hash !== expected.sha256)) {
102
- drift = `room role briefing drift at history seq ${record.seq}`;
103
- continue;
104
- }
105
- if (record.briefing_version === expected.version && hash === expected.sha256) {
106
- if (state.message_id && state.message_id !== record.message_id)
107
- drift = `multiple exact role briefing messages for ${expected.memberCid}`;
108
- else
109
- state.message_id = record.message_id;
110
- }
111
- continue;
112
- }
113
- if (record.kind === 'relay_intent' && state.message_id
114
- && record.message_id === state.message_id
115
- && record.recipient_identity === expected.memberCid) {
116
- state.relay_intent_record_id = record.record_id;
117
- continue;
118
- }
119
- if (record.kind === 'relay_result' && state.message_id && state.relay_intent_record_id
120
- && record.intent_record_id === state.relay_intent_record_id
121
- && record.message_id === state.message_id
122
- && record.recipient_identity === expected.memberCid) {
123
- state.relay_result_record_id = record.record_id;
124
- state.relay_wire_id = record.wire_id;
125
- state.state = record.status === 'queued' ? 'relay_queued' : 'relay_failed';
126
- continue;
127
- }
128
- if (record.kind !== 'message' || record.category !== 'chat'
129
- || record.author.identity !== expected.memberCid
130
- || !record.text.includes('fleet_room_briefing_ack'))
131
- continue;
132
- const ack = parseRoomBriefingAck(record.text);
133
- const mismatch = !ack ? 'invalid or non-canonical ACK JSON'
134
- : !state.message_id ? 'no matching room role briefing'
135
- : state.state !== 'relay_queued' ? 'briefing relay was not queued'
136
- : ackMismatch(ack, expected, state.message_id);
137
- if (mismatch) {
138
- state.rejected_ack_count++;
139
- state.last_rejected_ack_reason = mismatch;
140
- state.last_rejected_ack_seq = record.seq;
141
- }
142
- else {
143
- state.state = 'acknowledged';
144
- state.acknowledged_at = record.at;
145
- state.acknowledgement_message_id = record.message_id;
146
- state.acknowledgement_seq = record.seq;
147
- validAck ??= record;
148
- }
149
- }
150
- return { briefing: state, validAck, drift };
151
- }