@cello-protocol/daemon 0.0.122 → 0.0.124
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/dist/daemon.js +126 -17
- package/dist/daemon.js.map +1 -1
- package/dist/document-store.d.ts +0 -88
- package/dist/document-store.d.ts.map +1 -1
- package/dist/document-store.js +1 -344
- package/dist/document-store.js.map +1 -1
- package/dist/document-write-path.d.ts.map +1 -1
- package/dist/document-write-path.js +43 -10
- package/dist/document-write-path.js.map +1 -1
- package/dist/reconnect-drain.d.ts +7 -0
- package/dist/reconnect-drain.d.ts.map +1 -1
- package/dist/reconnect-drain.js +18 -4
- package/dist/reconnect-drain.js.map +1 -1
- package/dist/retry-queue.d.ts +16 -1
- package/dist/retry-queue.d.ts.map +1 -1
- package/dist/retry-queue.js +46 -9
- package/dist/retry-queue.js.map +1 -1
- package/dist/session-content-handlers.d.ts.map +1 -1
- package/dist/session-content-handlers.js +49 -11
- package/dist/session-content-handlers.js.map +1 -1
- package/dist/session-node-manager.d.ts +13 -1
- package/dist/session-node-manager.d.ts.map +1 -1
- package/dist/session-node-manager.js +162 -9
- package/dist/session-node-manager.js.map +1 -1
- package/package.json +4 -4
- package/dist/document-delivery-transport.d.ts +0 -78
- package/dist/document-delivery-transport.d.ts.map +0 -1
- package/dist/document-delivery-transport.js +0 -109
- package/dist/document-delivery-transport.js.map +0 -1
- package/dist/document-delivery.d.ts +0 -130
- package/dist/document-delivery.d.ts.map +0 -1
- package/dist/document-delivery.js +0 -246
- package/dist/document-delivery.js.map +0 -1
- package/dist/document-handshake.d.ts +0 -88
- package/dist/document-handshake.d.ts.map +0 -1
- package/dist/document-handshake.js +0 -239
- package/dist/document-handshake.js.map +0 -1
- package/dist/document-lifecycle.d.ts +0 -104
- package/dist/document-lifecycle.d.ts.map +0 -1
- package/dist/document-lifecycle.js +0 -363
- package/dist/document-lifecycle.js.map +0 -1
- package/dist/document-notify.d.ts +0 -130
- package/dist/document-notify.d.ts.map +0 -1
- package/dist/document-notify.js +0 -313
- package/dist/document-notify.js.map +0 -1
- package/dist/document-reachability.d.ts +0 -42
- package/dist/document-reachability.d.ts.map +0 -1
- package/dist/document-reachability.js +0 -72
- package/dist/document-reachability.js.map +0 -1
- package/dist/document-rejection.d.ts +0 -224
- package/dist/document-rejection.d.ts.map +0 -1
- package/dist/document-rejection.js +0 -374
- package/dist/document-rejection.js.map +0 -1
- package/dist/line-lcs.d.ts +0 -51
- package/dist/line-lcs.d.ts.map +0 -1
- package/dist/line-lcs.js +0 -71
- package/dist/line-lcs.js.map +0 -1
|
@@ -229,6 +229,46 @@ export class SessionNodeManager {
|
|
|
229
229
|
// still fires and the ACK still resolves — only the durable crash-backstop is skipped.
|
|
230
230
|
#onAwaitingPersisted = null;
|
|
231
231
|
#onAwaitingTtf = null;
|
|
232
|
+
// M12-P12 verification: force the next N park deposits to be REFUSED, so the failure this unit
|
|
233
|
+
// fixes can be produced on demand instead of waited for. The real failure is a race — the deposit
|
|
234
|
+
// is refused only in the seconds-long window while the sender's standing receiver rebuilds — and
|
|
235
|
+
// no CLI lever reaches that window: set-agent-offline leaves an open session's node serving, and
|
|
236
|
+
// the CLI refuses a send from an offline agent. Without this the fix ships unwatched.
|
|
237
|
+
// INERT unless the daemon is started with CELLO_FAULT_INJECTION=1; the IPC handler that sets it
|
|
238
|
+
// refuses outright otherwise, so a normal daemon cannot be talked into dropping messages.
|
|
239
|
+
#parkFaultRemaining = 0;
|
|
240
|
+
#parkFaultCause = "standing_receiver_creating";
|
|
241
|
+
// The incident needs BOTH halves: the direct dial has to fail (or the park path is never entered
|
|
242
|
+
// — measured, the counterparty's session node accepts the frame and reports delivered:true even
|
|
243
|
+
// with its agent away), and the park deposit that follows has to be refused. One without the
|
|
244
|
+
// other reproduces nothing.
|
|
245
|
+
#sendFaultRemaining = 0;
|
|
246
|
+
/** Arm the park-deposit fault. Returns the count now armed. */
|
|
247
|
+
injectParkFault(count, cause) {
|
|
248
|
+
this.#parkFaultRemaining = Math.max(0, count);
|
|
249
|
+
if (cause)
|
|
250
|
+
this.#parkFaultCause = cause;
|
|
251
|
+
return this.#parkFaultRemaining;
|
|
252
|
+
}
|
|
253
|
+
/** Arm the direct-send fault — makes the next N sends take the dial-failure path. */
|
|
254
|
+
injectSendFault(count) {
|
|
255
|
+
this.#sendFaultRemaining = Math.max(0, count);
|
|
256
|
+
return this.#sendFaultRemaining;
|
|
257
|
+
}
|
|
258
|
+
getSendFaultRemaining() {
|
|
259
|
+
return this.#sendFaultRemaining;
|
|
260
|
+
}
|
|
261
|
+
/** Remaining armed park faults — so a test can assert the fault was actually consumed. */
|
|
262
|
+
getParkFaultRemaining() {
|
|
263
|
+
return this.#parkFaultRemaining;
|
|
264
|
+
}
|
|
265
|
+
// M12-P12: the durable enqueue for a park deposit that FAILED. Distinct from onTtf because the
|
|
266
|
+
// cause is distinct — nothing timed out here, the deposit was refused — and an event named for
|
|
267
|
+
// the wrong cause is how this path stayed invisible.
|
|
268
|
+
// M12-P13 (review HIGH-1): returns whether the content is ACTUALLY queued. `false` means the
|
|
269
|
+
// queue dropped it (today: the content-derived dedupe key collided), and the caller must then not
|
|
270
|
+
// claim durability — nor commit the leaf that claim now authorises.
|
|
271
|
+
#onParkFailed = null;
|
|
232
272
|
/**
|
|
233
273
|
* MSG-001-3b (2b): the live content-park deposit. The manager resolves the recipient + relay
|
|
234
274
|
* endpoint from the session entry and calls this when a send is NOT confirmed delivered
|
|
@@ -277,6 +317,7 @@ export class SessionNodeManager {
|
|
|
277
317
|
setAwaitingAckHooks(hooks) {
|
|
278
318
|
this.#onAwaitingPersisted = hooks.onPersisted ?? null;
|
|
279
319
|
this.#onAwaitingTtf = hooks.onTtf ?? null;
|
|
320
|
+
this.#onParkFailed = hooks.onParkFailed ?? null;
|
|
280
321
|
}
|
|
281
322
|
/**
|
|
282
323
|
* MSG-001-3b (2b): inject the live content-park deposit (seal + ContentParkClient.deposit).
|
|
@@ -365,10 +406,27 @@ export class SessionNodeManager {
|
|
|
365
406
|
* result — the deposit itself and its logging are unchanged either way.
|
|
366
407
|
*/
|
|
367
408
|
async #parkContent(agentName, sessionId, contentHashHex, content, structure1Cbor, structure2Cbor) {
|
|
409
|
+
// Fault injection FIRST, so it reproduces the real shape: the refusal happens at the same point
|
|
410
|
+
// the live hook refuses (before any deposit), with the same event and the same `cause`.
|
|
411
|
+
if (this.#parkFaultRemaining > 0) {
|
|
412
|
+
this.#parkFaultRemaining -= 1;
|
|
413
|
+
this.#logger.warn("content.park.deposit.failed", {
|
|
414
|
+
sessionId,
|
|
415
|
+
contentHash: contentHashHex,
|
|
416
|
+
reason: "standing_receiver_unavailable",
|
|
417
|
+
cause: this.#parkFaultCause,
|
|
418
|
+
injected: true,
|
|
419
|
+
});
|
|
420
|
+
return { outcome: "refused", cause: this.#parkFaultCause };
|
|
421
|
+
}
|
|
368
422
|
const hook = this.#contentParkHook;
|
|
369
423
|
const entry = this.#activeNodes.get(this.#k(agentName, sessionId));
|
|
424
|
+
// M12-P12 (review F6): "no park target configured" is NOT a refused deposit. Content in a
|
|
425
|
+
// session with no relay was never recoverable through the park, so queuing it for re-park would
|
|
426
|
+
// be a lie that grows the DB forever — every boot and every agent start would retry a row whose
|
|
427
|
+
// only possible outcome is no_persisted_relay_endpoint. Reported as unconfigured, not refused.
|
|
370
428
|
if (!hook || !entry || !entry.relayPeerId || !entry.relayAddrs)
|
|
371
|
-
return
|
|
429
|
+
return { outcome: "unconfigured" };
|
|
372
430
|
try {
|
|
373
431
|
const result = await hook({
|
|
374
432
|
// SEC-1: the hook must sign as the SENDING agent — it needs to know who that is.
|
|
@@ -393,10 +451,11 @@ export class SessionNodeManager {
|
|
|
393
451
|
sessionId,
|
|
394
452
|
contentHash: contentHashHex,
|
|
395
453
|
reason: result.reason,
|
|
454
|
+
cause: result.cause,
|
|
396
455
|
});
|
|
397
|
-
return
|
|
456
|
+
return { outcome: "refused", cause: result.cause ?? result.reason };
|
|
398
457
|
}
|
|
399
|
-
return
|
|
458
|
+
return { outcome: "parked" };
|
|
400
459
|
}
|
|
401
460
|
catch (err) {
|
|
402
461
|
this.#logger.warn("content.park.deposit.failed", {
|
|
@@ -404,7 +463,7 @@ export class SessionNodeManager {
|
|
|
404
463
|
contentHash: contentHashHex,
|
|
405
464
|
error: err instanceof Error ? err.message : String(err),
|
|
406
465
|
});
|
|
407
|
-
return
|
|
466
|
+
return { outcome: "refused", cause: err instanceof Error ? err.message : String(err) };
|
|
408
467
|
}
|
|
409
468
|
}
|
|
410
469
|
// ─── Initialization ──────────────────────────────────────────────────────
|
|
@@ -2886,7 +2945,10 @@ export class SessionNodeManager {
|
|
|
2886
2945
|
async sendContent(agentName, sessionId, content, contentHash, correlationId) {
|
|
2887
2946
|
const entry = this.#activeNodes.get(this.#k(agentName, sessionId));
|
|
2888
2947
|
if (!entry) {
|
|
2889
|
-
|
|
2948
|
+
// M12-P13: no node, so nothing was witnessed and nothing was queued — the caller must NOT
|
|
2949
|
+
// commit a leaf for this. `durable` is a required field precisely so a new failure branch
|
|
2950
|
+
// cannot be added without answering the question every caller now asks.
|
|
2951
|
+
return { ok: false, reason: "session_node_unavailable", error: "no active session node for this session", durable: false };
|
|
2890
2952
|
}
|
|
2891
2953
|
// R1 (MSG-001-3b): witness the message-leaf HASH to the relay FIRST, INDEPENDENT of
|
|
2892
2954
|
// direct delivery. The relay is the ordering authority (Structure 2): it assigns the
|
|
@@ -2960,6 +3022,14 @@ export class SessionNodeManager {
|
|
|
2960
3022
|
structure1_cbor: orderingS1,
|
|
2961
3023
|
structure2_cbor: orderingS2,
|
|
2962
3024
|
});
|
|
3025
|
+
// Injected dial failure — thrown from inside the try so it lands in exactly the catch the
|
|
3026
|
+
// real connection_lost lands in, and the whole downstream path (untrack → park → durable
|
|
3027
|
+
// enqueue) runs unmodified.
|
|
3028
|
+
if (this.#sendFaultRemaining > 0) {
|
|
3029
|
+
this.#sendFaultRemaining -= 1;
|
|
3030
|
+
this.#logger.warn("content.send.fault.injected", { sessionId, contentHash: Buffer.from(contentHash).toString("hex") });
|
|
3031
|
+
throw new Error("connection_lost: injected direct-send fault");
|
|
3032
|
+
}
|
|
2963
3033
|
stream.send(lp.encode.single(frame));
|
|
2964
3034
|
try {
|
|
2965
3035
|
await stream.close();
|
|
@@ -2977,10 +3047,69 @@ export class SessionNodeManager {
|
|
|
2977
3047
|
// DOD-LEAVEMSG-1: the deposit is now AWAITED (was fire-and-forget) so a genuine park success
|
|
2978
3048
|
// can be reported as "dispatched to relay" instead of a raw stream failure — the operator/
|
|
2979
3049
|
// agent sees the truth (the message IS in flight, just not direct), not a false negative.
|
|
2980
|
-
const
|
|
2981
|
-
|
|
3050
|
+
const hashHex = Buffer.from(contentHash).toString("hex");
|
|
3051
|
+
const attempt = await this.#parkContent(agentName, sessionId, hashHex, content, orderingS1, orderingS2);
|
|
3052
|
+
if (attempt.outcome === "parked") {
|
|
2982
3053
|
return { ok: true, delivered: false, parked: true };
|
|
2983
3054
|
}
|
|
3055
|
+
// M12-P12: the deposit was refused, and #untrackAwaitingAck above already dropped the
|
|
3056
|
+
// in-memory entry — so without this, NOTHING holds the content and the TTF timer that would
|
|
3057
|
+
// have enqueued it is cancelled. The recipient has already witnessed this sequence, so it
|
|
3058
|
+
// holds every later message in the session behind the gap, forever, and tells no one.
|
|
3059
|
+
// Enqueue durably instead; the drain hook re-parks it the moment the standing receiver is
|
|
3060
|
+
// rebuilt. Only on a REFUSAL — a successful deposit must not be re-parked, and an
|
|
3061
|
+
// unconfigured session has no park target to retry against (F6).
|
|
3062
|
+
let durable = false;
|
|
3063
|
+
if (attempt.outcome === "refused") {
|
|
3064
|
+
try {
|
|
3065
|
+
// M12-P13 (review HIGH-1): `durable` is now OBSERVED from the enqueue, not asserted around
|
|
3066
|
+
// it. Two ways this used to lie, both of which now commit a chain leaf and so cannot be
|
|
3067
|
+
// allowed to: the queue's content-derived dedupe key collides and it silently drops the
|
|
3068
|
+
// copy, and the `?.` no-ops entirely when the composition root never wired the hook. An
|
|
3069
|
+
// absent hook is not a queue.
|
|
3070
|
+
if (this.#onParkFailed === null) {
|
|
3071
|
+
this.#logger.error("content.park.durable_enqueue.unwired", {
|
|
3072
|
+
sessionId, contentHash: hashHex, agentName,
|
|
3073
|
+
impact: "no durable queue is wired — the content is NOT retained and will NOT be retried",
|
|
3074
|
+
});
|
|
3075
|
+
}
|
|
3076
|
+
else {
|
|
3077
|
+
durable = this.#onParkFailed(agentName, sessionId, hashHex, content, orderingS1, orderingS2);
|
|
3078
|
+
}
|
|
3079
|
+
if (!durable) {
|
|
3080
|
+
if (this.#onParkFailed !== null) {
|
|
3081
|
+
this.#logger.error("content.park.durable_enqueue.dropped", {
|
|
3082
|
+
sessionId, contentHash: hashHex, agentName,
|
|
3083
|
+
impact: "the durable queue refused this copy (identical content already queued) — it is NOT separately retained",
|
|
3084
|
+
});
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
else {
|
|
3088
|
+
// F5: the successful enqueue must be visible. Without this the live run that has to
|
|
3089
|
+
// PROVE this fix has nothing to point at, and this log is the sender-side counterpart to
|
|
3090
|
+
// `session.content.held` on the receiver — the two together make the trace readable.
|
|
3091
|
+
// M12-P13: `witnessed` rides along because the caller is about to commit a hash-chain
|
|
3092
|
+
// leaf on the strength of this. Without a relay ordering record the recipient recovers
|
|
3093
|
+
// in arrival order instead — the accepted degradation, but it must not be invisible.
|
|
3094
|
+
this.#logger.info("content.park.deferred", {
|
|
3095
|
+
sessionId, contentHash: hashHex, agentName,
|
|
3096
|
+
selfOrdering: Boolean(orderingS1 && orderingS2),
|
|
3097
|
+
witnessed: Boolean(orderingS1 && orderingS2),
|
|
3098
|
+
});
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
catch (hookErr) {
|
|
3102
|
+
// F3: enqueueAwaitingContent throws ON PURPOSE when the persist fails, because that is
|
|
3103
|
+
// data loss. Swallowing it into the same response the durable case returns would tell the
|
|
3104
|
+
// operator "it will retry" about a message that is simply gone. Named for its own cause,
|
|
3105
|
+
// and the response says so below.
|
|
3106
|
+
this.#logger.error("content.park.durable_enqueue.failed", {
|
|
3107
|
+
sessionId, contentHash: hashHex, agentName,
|
|
3108
|
+
impact: "content is NOT durable and will NOT be retried — the message is lost",
|
|
3109
|
+
error: hookErr instanceof Error ? hookErr.message : String(hookErr),
|
|
3110
|
+
});
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
2984
3113
|
// error.message extracted — never [object Object]. libp2p/cross-package errors are not
|
|
2985
3114
|
// always `instanceof Error` in this realm, so fall back to a message property / JSON.
|
|
2986
3115
|
const errMsg = err instanceof Error
|
|
@@ -2995,7 +3124,28 @@ export class SessionNodeManager {
|
|
|
2995
3124
|
return String(err);
|
|
2996
3125
|
}
|
|
2997
3126
|
})();
|
|
2998
|
-
|
|
3127
|
+
// F3: the two failures are NOT interchangeable to the caller. `reason` is a contract string
|
|
3128
|
+
// and stays put; `guidance` carries the difference, because "we are retrying this" and "this
|
|
3129
|
+
// message is gone, send it again" demand opposite actions from the operator.
|
|
3130
|
+
return {
|
|
3131
|
+
ok: false,
|
|
3132
|
+
reason: "session_stream_unavailable",
|
|
3133
|
+
error: errMsg,
|
|
3134
|
+
// M12-P13: the machine-readable half of the distinction below. M12-P12 shipped it in the
|
|
3135
|
+
// guidance SENTENCE only, so the callers that have to ACT on it — commit the leaf for a
|
|
3136
|
+
// queued message, never for a lost one — would have had to substring-match English. None
|
|
3137
|
+
// did, and the sequence the relay had already witnessed was left as a permanent hole.
|
|
3138
|
+
durable,
|
|
3139
|
+
// M12-P13 (review MEDIUM-5): the specific standing-receiver state, carried rather than
|
|
3140
|
+
// discarded. `reason` names where this surfaced; `cause` names what actually blocked it —
|
|
3141
|
+
// the exact distinction M12-P12 added `standingReceiverAbsenceReason()` for, which then
|
|
3142
|
+
// died inside #parkContent. An operator keying on `reason` alone is sent to the transport
|
|
3143
|
+
// when the blocker is the receiver.
|
|
3144
|
+
...(attempt.cause !== undefined ? { cause: attempt.cause } : {}),
|
|
3145
|
+
guidance: durable
|
|
3146
|
+
? "Direct delivery failed and the relay refused the hand-off, so the message is queued and will be re-sent automatically when the relay link is back. Do not re-send it: an identical re-send is not separately queued."
|
|
3147
|
+
: "Direct delivery failed and the message could NOT be queued for retry — it is lost. Send it again.",
|
|
3148
|
+
};
|
|
2999
3149
|
}
|
|
3000
3150
|
}
|
|
3001
3151
|
/**
|
|
@@ -3966,7 +4116,10 @@ export class SessionNodeManager {
|
|
|
3966
4116
|
this.#awaitingAck.delete(ackKey);
|
|
3967
4117
|
this.#logger.debug("content.delivery.ttf_expired", { sessionId, contentHash: hashHex });
|
|
3968
4118
|
try {
|
|
3969
|
-
|
|
4119
|
+
// M12-P12 (review pass 2): the ordering record travels on THIS path too. It is in hand — the
|
|
4120
|
+
// very next statement hands it to #parkContent — and a TTF row written without it re-parks in
|
|
4121
|
+
// arrival order, which is the divergent-leaf-index failure the durable columns exist to stop.
|
|
4122
|
+
this.#onAwaitingTtf?.(agentName, sessionId, hashHex, entry.content, entry.structure1Cbor, entry.structure2Cbor);
|
|
3970
4123
|
}
|
|
3971
4124
|
catch (err) {
|
|
3972
4125
|
this.#logger.error("content.park.backstop.failed", {
|