Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions changelog.d/2855-room-read-take-line-seq.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`read_group_chat` no longer counts a share-seq reservation as a line the room said: a mention that reserves a remote line number writes nothing to the room log, so reserving one after a five-line log used to read `Latest in "Ops" (newest 5 of 6, oldest first):` and grew one phantom line per mention; the header stays `Latest in "Ops" (oldest first):` and the reservation still gets a new number, so a second mention is never a redelivery. A room whose log really was trimmed still reports the lines the cap dropped. See [#2855](https://github.com/ThinkFlowLab/FlyBot/issues/2855).
18 changes: 12 additions & 6 deletions core/src/host/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -1244,17 +1244,23 @@ export function install(rt) {
} : {}),
}))
},
// #1126: the next share seq for a mention. Same counter a stored line uses (`room.seq`), so a
// second mention is a new remote line instead of a redelivery of seq 0.
// #1126: the next share seq for a mention. A reservation writes no line, so it must not move
// `room.seq`: that counter is how many lines the room said, and the room read subtracts the
// retained log from it to report what the cap dropped — a reservation on the same counter read
// as a line that had been cut (#2855). `shareSeq` starts above everything already said, so a
// second mention is still a new remote line instead of a redelivery of seq 0.
takeLineSeq(roomId) {
const room = rt.roomOf?.(String(roomId ?? ''))
if (!room) return 1
if (typeof room.seq !== 'number' || !Number.isFinite(room.seq) || room.seq < 0) {
room.seq = Array.isArray(room.log) ? room.log.length : 0
if (typeof room.shareSeq !== 'number' || !Number.isFinite(room.shareSeq) || room.shareSeq < 0) {
const said = typeof room.seq === 'number' && Number.isFinite(room.seq) && room.seq >= 0
? room.seq
: Array.isArray(room.log) ? room.log.length : 0
room.shareSeq = said
}
room.seq += 1
room.shareSeq += 1
void Promise.resolve(rt.save?.()).catch(() => {})
return room.seq
return room.shareSeq
},
removeParticipant: (roomId, ref) => rt.removeRoomParticipant?.(roomId, ref),
upsertFace: spec => rt.upsertFaceRoom?.(spec),
Expand Down
17 changes: 17 additions & 0 deletions core/test/room-read-trimmed-count.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,20 @@ test('P-684 guard: a room under the cap is byte-identical', async (t) => {
const out = await host.tool('read_group_chat', { group: 'Ops' }, 'gb')
assert.equal(out, `Latest in "Ops" (oldest first):\n${said.map(l => `${l.who}: ${l.text}`).join('\n')}`)
})

// RED today (#2855): `takeLineSeq` reserves a remote line number for a co-seat mention and never writes
// a line, so a gap the counter opened is read as a line the cap dropped and every reservation adds one
// phantom line to the header. A read must not report a cut that never happened.
test('#2855: a share-seq reservation writes no line, so the read is byte-identical', async (t) => {
const said = lines(5)
const host = await start(t, said)
const before = await host.tool('read_group_chat', { group: 'Ops' }, 'gb')
assert.equal(before, `Latest in "Ops" (oldest first):\n${said.map(l => `${l.who}: ${l.text}`).join('\n')}`)
const reserved = host.rt.flybot.rooms.takeLineSeq('r1')
assert.equal(reserved, 6, 'the reservation continues the room counter')
const after = await host.tool('read_group_chat', { group: 'Ops' }, 'gb')
assert.equal(after, before, 'a reservation writes no line, so nothing was left out')
// A second reservation stays above the first and still writes nothing.
assert.equal(host.rt.flybot.rooms.takeLineSeq('r1'), 7)
assert.equal(await host.tool('read_group_chat', { group: 'Ops' }, 'gb'), before)
})
2 changes: 1 addition & 1 deletion docs/subsystems/core-host.md
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@ has lines (#2790, `core/test/room-read-floor-clamp.test.js`).

### Group reads say what a cap left out (#2780)

- `read_group_chat` shows at most `ROOM_READ_MAX` (30) of the room lines visible to the caller, the newest ones, and the cut used to be silent: `Latest in "<room>" (oldest first):` printed as if those 30 were the whole log, so a Bot asked "didn't we decide X?" read a truncated log as the whole log — the same failure #1906 fixed for the own-chat read. The header now names the cut, `Latest in "<room>" (newest 30 of 45, oldest first):`, and a read that fits the cap stays byte-identical; lines before the member's `joinedSeq` are not counted as left out, and a room whose log was trimmed counts the lines it dropped (the store keeps only the newest `ROOM_LOG_MAX` lines, 60 here and 200 in `room-membership.js`, while `room.seq` keeps counting every line, so 500 lines said and 60 kept read `newest 30 of 500`). Covered by `core/test/room-read-honest-count.test.js`, `core/test/room-read-trimmed-count.test.js` and `core/test/group-work.test.js`.
- `read_group_chat` shows at most `ROOM_READ_MAX` (30) of the room lines visible to the caller, the newest ones, and the cut used to be silent: `Latest in "<room>" (oldest first):` printed as if those 30 were the whole log, so a Bot asked "didn't we decide X?" read a truncated log as the whole log — the same failure #1906 fixed for the own-chat read. The header now names the cut, `Latest in "<room>" (newest 30 of 45, oldest first):`, and a read that fits the cap stays byte-identical; lines before the member's `joinedSeq` are not counted as left out, and a room whose log was trimmed counts the lines it dropped (the store keeps only the newest `ROOM_LOG_MAX` lines, 60 here and 200 in `room-membership.js`, while `room.seq` keeps counting every line, so 500 lines said and 60 kept read `newest 30 of 500`). A share-seq reservation for a co-seat mention writes no line, so it keeps its own counter (`room.shareSeq`, starting above everything already said) and never moves `room.seq`: counting a reservation there read as a line the cap had dropped and grew the header by one per mention (#2855). Covered by `core/test/room-read-honest-count.test.js`, `core/test/room-read-trimmed-count.test.js` and `core/test/group-work.test.js`.

### Chat anchors name their part (#2710)

Expand Down
2 changes: 1 addition & 1 deletion docs/subsystems/plugin-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ FlyBot's plugin API surface on top of the patched core: host hooks, client slots
### Inbound shared rooms on `flybot.rooms`

- `flybot.rooms.applyInbound` turns a Bot Social inbox envelope into the owner's inbound-share projection; `leaveInbound` hides those rooms when the invited Bot is removed or its grant is revoked. Face-to-face methods are unchanged. Still not team mode.
- `flybot.rooms.serveInvites({ identify, mint, preview, claim, verify, revoke })` registers Bot Social's record of a local room's invite tickets (#884; `rt.roomInviteServer`, see core-host's "A local room's invite link"); it returns the unregister function. Registering it also runs `rt.identifyInviteBots` once (#899), so a link minted on a room-only Bot gets that Bot's full identity through `identify`. `mint` answers `undefined` when the server has no `local_room_invites`, and a refusal throws an Error whose `code` is Bot Social's. `flybot.rooms.seatOpenClaims(opts)` reads those claims and seats them without waiting for the accepting knock (#1064; `rt.seatOpenClaims`). `flybot.rooms.ticketPeers()` lists the pairs a link opens (`{ from, to, scope, ticket }`, reach.js `ticketPeers`), so a transport can let a room share through to exactly those, under `scope`, and to nobody else off its roster. `flybot.rooms.takeLineSeq(roomId)` advances that room's `seq` the same way a stored line does and returns the new number (#1126), so a shared-room mention's envelope is a new remote line. `list()` copies an inbound mirror's `sharedFrom.remoteRoomId`, `ticket` and `inviterHandle` for that envelope.
- `flybot.rooms.serveInvites({ identify, mint, preview, claim, verify, revoke })` registers Bot Social's record of a local room's invite tickets (#884; `rt.roomInviteServer`, see core-host's "A local room's invite link"); it returns the unregister function. Registering it also runs `rt.identifyInviteBots` once (#899), so a link minted on a room-only Bot gets that Bot's full identity through `identify`. `mint` answers `undefined` when the server has no `local_room_invites`, and a refusal throws an Error whose `code` is Bot Social's. `flybot.rooms.seatOpenClaims(opts)` reads those claims and seats them without waiting for the accepting knock (#1064; `rt.seatOpenClaims`). `flybot.rooms.ticketPeers()` lists the pairs a link opens (`{ from, to, scope, ticket }`, reach.js `ticketPeers`), so a transport can let a room share through to exactly those, under `scope`, and to nobody else off its roster. `flybot.rooms.takeLineSeq(roomId)` advances that room's own `shareSeq` — starting above everything already said, never `room.seq` — and returns the new number (#1126, #2855), so a shared-room mention's envelope is a new remote line while a reservation writes no line and cannot be read as one the log dropped. `list()` copies an inbound mirror's `sharedFrom.remoteRoomId`, `ticket` and `inviterHandle` for that envelope.

### Face-to-face owner lines

Expand Down
18 changes: 12 additions & 6 deletions patches/ds-bot/files/src/host/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -1244,17 +1244,23 @@ export function install(rt) {
} : {}),
}))
},
// #1126: the next share seq for a mention. Same counter a stored line uses (`room.seq`), so a
// second mention is a new remote line instead of a redelivery of seq 0.
// #1126: the next share seq for a mention. A reservation writes no line, so it must not move
// `room.seq`: that counter is how many lines the room said, and the room read subtracts the
// retained log from it to report what the cap dropped — a reservation on the same counter read
// as a line that had been cut (#2855). `shareSeq` starts above everything already said, so a
// second mention is still a new remote line instead of a redelivery of seq 0.
takeLineSeq(roomId) {
const room = rt.roomOf?.(String(roomId ?? ''))
if (!room) return 1
if (typeof room.seq !== 'number' || !Number.isFinite(room.seq) || room.seq < 0) {
room.seq = Array.isArray(room.log) ? room.log.length : 0
if (typeof room.shareSeq !== 'number' || !Number.isFinite(room.shareSeq) || room.shareSeq < 0) {
const said = typeof room.seq === 'number' && Number.isFinite(room.seq) && room.seq >= 0
? room.seq
: Array.isArray(room.log) ? room.log.length : 0
room.shareSeq = said
}
room.seq += 1
room.shareSeq += 1
void Promise.resolve(rt.save?.()).catch(() => {})
return room.seq
return room.shareSeq
},
removeParticipant: (roomId, ref) => rt.removeRoomParticipant?.(roomId, ref),
upsertFace: spec => rt.upsertFaceRoom?.(spec),
Expand Down
17 changes: 17 additions & 0 deletions patches/ds-bot/files/test/room-read-trimmed-count.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,20 @@ test('P-684 guard: a room under the cap is byte-identical', async (t) => {
const out = await host.tool('read_group_chat', { group: 'Ops' }, 'gb')
assert.equal(out, `Latest in "Ops" (oldest first):\n${said.map(l => `${l.who}: ${l.text}`).join('\n')}`)
})

// RED today (#2855): `takeLineSeq` reserves a remote line number for a co-seat mention and never writes
// a line, so a gap the counter opened is read as a line the cap dropped and every reservation adds one
// phantom line to the header. A read must not report a cut that never happened.
test('#2855: a share-seq reservation writes no line, so the read is byte-identical', async (t) => {
const said = lines(5)
const host = await start(t, said)
const before = await host.tool('read_group_chat', { group: 'Ops' }, 'gb')
assert.equal(before, `Latest in "Ops" (oldest first):\n${said.map(l => `${l.who}: ${l.text}`).join('\n')}`)
const reserved = host.rt.flybot.rooms.takeLineSeq('r1')
assert.equal(reserved, 6, 'the reservation continues the room counter')
const after = await host.tool('read_group_chat', { group: 'Ops' }, 'gb')
assert.equal(after, before, 'a reservation writes no line, so nothing was left out')
// A second reservation stays above the first and still writes nothing.
assert.equal(host.rt.flybot.rooms.takeLineSeq('r1'), 7)
assert.equal(await host.tool('read_group_chat', { group: 'Ops' }, 'gb'), before)
})
Loading