@cello-protocol/daemon 0.0.35 → 0.0.36

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.
@@ -1 +1 @@
1
- {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH,OAAO,KAAK,EACV,YAAY,EACZ,oBAAoB,EAQrB,MAAM,YAAY,CAAC;AAIpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAQ/D,OAAO,EAAoD,KAAK,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAsB7G,OAAO,KAAK,EAAE,kBAAkB,EAA+C,MAAM,yBAAyB,CAAC;AAO/G,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,2BAA2B,CAAC;AA0InF,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,SAAS,IAAI,oBAAoB,CAAC;IAClC;;;;OAIG;IACH,qBAAqB,IAAI,kBAAkB,CAAC;IAC5C;;;;;;;;OAQG;IACH,gBAAgB,IAAI,SAAS,GAAG,IAAI,CAAC;IACrC;;;;OAIG;IACH,oBAAoB,IAAI,kBAAkB,CAAC;IAC3C;;;OAGG;IACH,iBAAiB,IAAI,eAAe,CAAC;CACtC;AA2CD,eAAO,MAAM,sBAAsB,QAAsB,CAAC;AAE1D,wBAAsB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CA+7K7E"}
1
+ {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH,OAAO,KAAK,EACV,YAAY,EACZ,oBAAoB,EAQrB,MAAM,YAAY,CAAC;AAIpB,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAQ/D,OAAO,EAAoD,KAAK,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAsB7G,OAAO,KAAK,EAAE,kBAAkB,EAA+C,MAAM,yBAAyB,CAAC;AAO/G,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,2BAA2B,CAAC;AA0InF,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,SAAS,IAAI,oBAAoB,CAAC;IAClC;;;;OAIG;IACH,qBAAqB,IAAI,kBAAkB,CAAC;IAC5C;;;;;;;;OAQG;IACH,gBAAgB,IAAI,SAAS,GAAG,IAAI,CAAC;IACrC;;;;OAIG;IACH,oBAAoB,IAAI,kBAAkB,CAAC;IAC3C;;;OAGG;IACH,iBAAiB,IAAI,eAAe,CAAC;CACtC;AA2CD,eAAO,MAAM,sBAAsB,QAAsB,CAAC;AAE1D,wBAAsB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAi9K7E"}
package/dist/daemon.js CHANGED
@@ -1559,13 +1559,19 @@ export async function startDaemon(config) {
1559
1559
  // no background timer). A session the standing receiver opened from an inbound offer the initiator
1560
1560
  // ABANDONED stays "active" forever (the counterparty never joins), clutters the open/active lists, and
1561
1561
  // its normal close fires an unsealable bilateral seal. Mark such a session terminal ("abandoned") once
1562
- // it is PROVABLY dead: status active + counterparty never established (liveness != "alive" AND 0
1563
- // RECEIVED messages — message_count alone counts our own auto-"Dispatched." ack, so it is NOT the
1564
- // signal) + age past the grace TTL (a genuinely fresh session still setting up must survive).
1562
+ // it is PROVABLY dead: counterparty never established (liveness != "alive" AND 0 RECEIVED messages —
1563
+ // message_count alone counts our own auto-"Dispatched." ack, so it is NOT the signal) + age past the
1564
+ // grace TTL (a genuinely fresh session still setting up must survive).
1565
+ // CC-10 (live 2026-07-08 Phase-2 block): scan 'interrupted' too, not just 'active'. A daemon restart
1566
+ // flips dead half-opens to 'interrupted'; those classify as "failed" (invisible in every list) yet
1567
+ // still count toward the unknown-sender acceptance bound (D18 deliberately counts 'interrupted') — so
1568
+ // a stranger whose first handshakes died was silently locked out FOREVER. Reaping only 0-RECEIVED
1569
+ // ghosts keeps D18 intact: the disconnect-evasion attacker's sessions always carry received content.
1565
1570
  const HALF_OPEN_TTL_MS = Number(process.env["CELLO_HALF_OPEN_TTL_MS"]) || 5 * 60 * 1000;
1566
1571
  function reapDeadHalfOpenSessions(agentName) {
1567
1572
  const now = Date.now();
1568
- for (const row of sessionNodeManager.getSessionsByStatus("active")) {
1573
+ const candidates = [...sessionNodeManager.getSessionsByStatus("active"), ...sessionNodeManager.getSessionsByStatus("interrupted")];
1574
+ for (const row of candidates) {
1569
1575
  if (agentName !== undefined && row.agent_name !== agentName)
1570
1576
  continue;
1571
1577
  if (now - row.created_at <= HALF_OPEN_TTL_MS)
@@ -1575,11 +1581,18 @@ export async function startDaemon(config) {
1575
1581
  if (sessionNodeManager.countReceivedMessages(row.agent_name, row.session_id) > 0)
1576
1582
  continue; // counterparty spoke
1577
1583
  // Non-awaited: abandonSession flips the DB status synchronously (before its first await), so THIS
1578
- // read reflects it; the async node teardown finishes in the background.
1579
- void sessionNodeManager.abandonSession(row.agent_name, row.session_id).catch((err) => {
1584
+ // read reflects it; the async node teardown finishes in the background. CC-10 reviewer LOW: only
1585
+ // log "reaped" if the status flip actually wrote — a swallowed write failure already logs
1586
+ // session.status.write.failed, and reporting success over it would hide a still-counting ghost.
1587
+ void sessionNodeManager.abandonSession(row.agent_name, row.session_id)
1588
+ .then((flipped) => {
1589
+ if (flipped) {
1590
+ logger.info("session.half_open.reaped", { agentName: row.agent_name, sessionId: row.session_id, priorStatus: row.status, ageMs: now - row.created_at });
1591
+ }
1592
+ })
1593
+ .catch((err) => {
1580
1594
  logger.warn("session.half_open.reap.failed", { agentName: row.agent_name, sessionId: row.session_id, reason: err instanceof Error ? err.message : String(err) });
1581
1595
  });
1582
- logger.info("session.half_open.reaped", { agentName: row.agent_name, sessionId: row.session_id, ageMs: now - row.created_at });
1583
1596
  }
1584
1597
  }
1585
1598
  // M8B F16: per-session liveness for ACTIVE sessions, shared by both status surfaces
@@ -4044,6 +4057,11 @@ export async function startDaemon(config) {
4044
4057
  // senders — a per-sender cap (many sessions from ONE stranger) and a global cap (many
4045
4058
  // sessions across ALL strangers combined). Known contacts are exempt ("bounded only by
4046
4059
  // disk" — DoD). Checked FIRST, before any standing-receiver work, so a refusal is cheap.
4060
+ // CC-10: reap provably-dead ghosts for THIS agent before counting — otherwise invisible
4061
+ // interrupted 0-received sessions (post-restart shape) consume the sender's budget forever
4062
+ // and lock the stranger out with no list read ever clearing them. Safe here: abandonSession
4063
+ // flips the DB status synchronously, so the bound query below sees the reaped state.
4064
+ reapDeadHalfOpenSessions(agentName);
4047
4065
  const bound = sessionNodeManager.checkUnknownSenderAcceptanceBound(agentName, parsed.participantAPubkeyHex);
4048
4066
  if (!bound.ok) {
4049
4067
  logger.warn("session.inbound.accept.failed", {