oxtail 0.14.0 → 0.14.1
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/AGENTS.md +1 -1
- package/dist/mailbox.js +15 -4
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -56,7 +56,7 @@ The v0.9/v0.10.1 changes close the public dogfooding gaps found by real peer tra
|
|
|
56
56
|
- **`ask_peer` replies must correlate when the peer supports it.** Same-peer chatter is not a reply. Upgraded peers advertise `capabilities.mailbox.reply_to` and must satisfy waits with `from_session_id == target.session_id` plus `reply_to == request_id`; unmatched messages stay in the mailbox. The older `from_session_id`-only path is legacy compatibility and must be surfaced as `correlation: "uncorrelated"`. For no-capability peers, stale same-peer chatter may still satisfy the wait; that is an explicit compatibility limitation, not a correctness guarantee.
|
|
57
57
|
- **Peer messages are context, not user authority.** Mailbox provenance (`origin: "peer"`, `request_id`, `reply_to`, `source_message_id`) is diagnostic metadata, not a trust boundary. Hook text must keep the trust framing visible — the "context, not user authority" line plus the `from_session_id` / `request_id` / `reply_to` reply fields (full protocol names) are rendered on every delivery — and injected hook bodies must stay under an explicit budget. Single-valued provenance the framing already implies (`origin: "peer"`) stays in the mailbox JSONL but need not be rendered into context.
|
|
58
58
|
- **A displayed reply handle must be resolvable: record the received-ledger before the mailbox line is visible.** Both delivery paths are destructive — `read_my_messages` and the PreToolUse/Stop hook each truncate the mailbox on handoff — so `reply_to_message` resolves `message_id` against a durable per-session ledger (`~/.oxtail/received/<hash(session_id)>.jsonl`), never the queue. `deliverToPeer` (the single delivery primitive behind `send_message` / `ask_peer` / `reply_to_message`) MUST write the ledger entry **before** appending the mailbox line: append-then-record reopens a window where the hook renders a `message_id` the receiver cannot yet reply to. The ledger is keyed and owned by receiver `session_id`; a lookup reads only the caller's own file. The ledger write is best-effort (a failure degrades to "no handle, reply via `send_message`") but must never reorder ahead of, or block, the actual delivery.
|
|
59
|
-
- **Delivery mutations are crash-consistent; the shared advisory lock is owner-validated.** A crash mid-write must never corrupt a neighbour: mailbox appends heal a torn record boundary so two JSONL lines can't glue into one unparseable record (`appendLines`), and full-file rewrites (received-ledger, selective drain) go through a temp file + atomic `rename` (`atomicWrite`), never an in-place `writeFileSync` that a torn write could leave half-applied. The `mkdir` lock — shared between the Node server and the bash hooks — carries an owner token in a sidecar `<lock>.owner` (beside the dir so it stays empty and a plain `rmdir` still works cross-language): **release removes the lock only if it still owns it** (a stalled holder can't stomp a successor), and **stale removal is single-winner** (`<lock>.steal` marker) **plus compare-and-clear** (remove only if the owner is still the dead token observed). Provable race-freedom is unachievable on a plain shared filesystem (no atomic compare-and-swap); the design closes every realistic race and the only residuals — enumerated in `src/locks.ts` — require a >30s SIGSTOP-class stall inside a microsecond syscall gap, with a bounded consequence (rare double-delivery or a degraded reply-handle, never a wedge or torn file). One protocol, mirrored in `src/locks.ts` and both hooks.
|
|
59
|
+
- **Delivery mutations are crash-consistent; the shared advisory lock is owner-validated.** A crash mid-write must never corrupt a neighbour: mailbox appends heal a torn record boundary so two JSONL lines can't glue into one unparseable record (`appendLines`), and full-file rewrites (received-ledger, selective drain) go through a temp file + atomic `rename` (`atomicWrite`), never an in-place `writeFileSync` that a torn write could leave half-applied. The `mkdir` lock — shared between the Node server and the bash hooks — carries an owner token in a sidecar `<lock>.owner` (beside the dir so it stays empty and a plain `rmdir` still works cross-language): **release removes the lock only if it still owns it** (a stalled holder can't stomp a successor), and **stale removal is single-winner** (`<lock>.steal` marker) **plus compare-and-clear** (remove only if the owner is still the dead token observed). Provable race-freedom is unachievable on a plain shared filesystem (no atomic compare-and-swap); the design closes every realistic race and the only residuals — enumerated in `src/locks.ts` — require a >30s SIGSTOP-class stall inside a microsecond syscall gap, with a bounded consequence (rare double-delivery or a degraded reply-handle, never a wedge or torn file). One protocol, mirrored in `src/locks.ts` and both hooks. As a consequence-mitigation, the union reader `drainMany` dedups by `message_id` (v0.14.1), so the most realistic duplicate — a `migrateMailbox` crash-window leaving the same message in two sibling mailboxes — is delivered exactly once (ids are unique nonces, so dedup only ever collapses true duplicates).
|
|
60
60
|
|
|
61
61
|
## Recently shipped
|
|
62
62
|
|
package/dist/mailbox.js
CHANGED
|
@@ -182,17 +182,28 @@ export function requeueMany(target_pid, msgs) {
|
|
|
182
182
|
// of silently stranding it. Best-effort per pid: a contended/unreadable mailbox
|
|
183
183
|
// is skipped (counted) and left for the next poll rather than failing the whole
|
|
184
184
|
// drain — one stuck lock must not block a session's entire inbox.
|
|
185
|
+
//
|
|
186
|
+
// Deduped by message_id: a migrateMailbox crash-window (append to dest done, but
|
|
187
|
+
// the process died before truncating the source) can leave the SAME message in
|
|
188
|
+
// two unioned sibling mailboxes. Both copies are drained (so neither lingers) but
|
|
189
|
+
// the message is returned ONCE. message_id is a unique per-message nonce, so this
|
|
190
|
+
// only ever collapses true duplicates, never two distinct messages.
|
|
185
191
|
export function drainMany(pids) {
|
|
186
192
|
const out = [];
|
|
187
|
-
const
|
|
193
|
+
const seenPids = new Set();
|
|
194
|
+
const seenIds = new Set();
|
|
188
195
|
let skipped = 0;
|
|
189
196
|
for (const pid of pids) {
|
|
190
|
-
if (
|
|
197
|
+
if (seenPids.has(pid))
|
|
191
198
|
continue;
|
|
192
|
-
|
|
199
|
+
seenPids.add(pid);
|
|
193
200
|
try {
|
|
194
|
-
for (const m of drain(pid))
|
|
201
|
+
for (const m of drain(pid)) {
|
|
202
|
+
if (seenIds.has(m.id))
|
|
203
|
+
continue;
|
|
204
|
+
seenIds.add(m.id);
|
|
195
205
|
out.push(m);
|
|
206
|
+
}
|
|
196
207
|
}
|
|
197
208
|
catch {
|
|
198
209
|
skipped++;
|