@cello-protocol/daemon 0.0.148 → 0.0.150

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.
@@ -0,0 +1,51 @@
1
+ /**
2
+ * DOD-AWAY-MUTUAL-SEAL-1 — recognising this daemon's own away auto-replies.
3
+ *
4
+ * ── WHY THIS EXISTS ──────────────────────────────────────────────────────────────────────────────
5
+ *
6
+ * When two agents contact each other while BOTH are unattended, each side's away responder answers
7
+ * the other's away responder. The second such arrival looks exactly like "a caller who ignored the
8
+ * leave-a-message instruction", which is what `DOD-INBOX-ONESHOT-1` exists to handle — so both sides
9
+ * send a `[[WRAP]]` rejection and initiate a seal. Two distinct-sender SEAL ctrl leaves is precisely
10
+ * what triggers notarization.
11
+ *
12
+ * Measured on the relay's own log 2026-08-09: a session **sealed three seconds after it opened**,
13
+ * its entire content being two machines telling each other nobody was home. Neither daemon learned
14
+ * (the seal completion is pushed with no pull twin), so both kept showing `active`; the operators
15
+ * came back, talked into a closed room for 68 minutes, and found out only at close.
16
+ *
17
+ * The one-shot rule is right about a human who keeps typing. It is wrong about another away
18
+ * responder, which is not a caller ignoring anything.
19
+ *
20
+ * ── WHY EXACT MATCHING, AND NOT A MARKER ─────────────────────────────────────────────────────────
21
+ *
22
+ * A marker in the wire frame would be cleaner and is the right long-term answer, but it is a wire
23
+ * change: an older peer would not send it, and this defect fires precisely when talking to a peer we
24
+ * do not control. Exact text match works today against the case that actually loops — both sides
25
+ * running THIS logic — and needs no version agreement.
26
+ *
27
+ * EXACT, never substring. A human who quotes the away message must still be answered; silencing a
28
+ * real message is a worse failure than the one being fixed, because the operator would never learn
29
+ * it arrived.
30
+ *
31
+ * The texts live HERE and are read by both the sender and the detector. A second hardcoded copy is
32
+ * how a reworded away message stops being recognised and the loop quietly returns.
33
+ */
34
+ /** The exact strings this daemon sends as away auto-replies. */
35
+ export declare const AWAY_AUTO_REPLY_TEXTS: {
36
+ /**
37
+ * The one-shot acknowledgement, sent when a message arrives for an unattended agent.
38
+ * States the one-shot rule, because without it a cooperative caller has no reason to stop.
39
+ */
40
+ readonly oneShot: string;
41
+ /** The session-offer answer, which names the away agent — so it is built, not fixed. */
42
+ readonly offerFor: (agentName: string) => string;
43
+ };
44
+ /**
45
+ * True when `text` is one of THIS daemon's away auto-replies — machine traffic, not a caller.
46
+ *
47
+ * The caller must neither answer it nor count it toward the one-shot rule. Answering produces the
48
+ * ping-pong; counting it produces the seal.
49
+ */
50
+ export declare function isOwnAwayAutoReply(text: string): boolean;
51
+ //# sourceMappingURL=away-detection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"away-detection.d.ts","sourceRoot":"","sources":["../src/away-detection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,gEAAgE;AAChE,eAAO,MAAM,qBAAqB;IAChC;;;OAGG;;IAKH,wFAAwF;mCACpE,MAAM,KAAG,MAAM;CAG3B,CAAC;AAKX;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAOxD"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * DOD-AWAY-MUTUAL-SEAL-1 — recognising this daemon's own away auto-replies.
3
+ *
4
+ * ── WHY THIS EXISTS ──────────────────────────────────────────────────────────────────────────────
5
+ *
6
+ * When two agents contact each other while BOTH are unattended, each side's away responder answers
7
+ * the other's away responder. The second such arrival looks exactly like "a caller who ignored the
8
+ * leave-a-message instruction", which is what `DOD-INBOX-ONESHOT-1` exists to handle — so both sides
9
+ * send a `[[WRAP]]` rejection and initiate a seal. Two distinct-sender SEAL ctrl leaves is precisely
10
+ * what triggers notarization.
11
+ *
12
+ * Measured on the relay's own log 2026-08-09: a session **sealed three seconds after it opened**,
13
+ * its entire content being two machines telling each other nobody was home. Neither daemon learned
14
+ * (the seal completion is pushed with no pull twin), so both kept showing `active`; the operators
15
+ * came back, talked into a closed room for 68 minutes, and found out only at close.
16
+ *
17
+ * The one-shot rule is right about a human who keeps typing. It is wrong about another away
18
+ * responder, which is not a caller ignoring anything.
19
+ *
20
+ * ── WHY EXACT MATCHING, AND NOT A MARKER ─────────────────────────────────────────────────────────
21
+ *
22
+ * A marker in the wire frame would be cleaner and is the right long-term answer, but it is a wire
23
+ * change: an older peer would not send it, and this defect fires precisely when talking to a peer we
24
+ * do not control. Exact text match works today against the case that actually loops — both sides
25
+ * running THIS logic — and needs no version agreement.
26
+ *
27
+ * EXACT, never substring. A human who quotes the away message must still be answered; silencing a
28
+ * real message is a worse failure than the one being fixed, because the operator would never learn
29
+ * it arrived.
30
+ *
31
+ * The texts live HERE and are read by both the sender and the detector. A second hardcoded copy is
32
+ * how a reworded away message stops being recognised and the loop quietly returns.
33
+ */
34
+ /** The exact strings this daemon sends as away auto-replies. */
35
+ export const AWAY_AUTO_REPLY_TEXTS = {
36
+ /**
37
+ * The one-shot acknowledgement, sent when a message arrives for an unattended agent.
38
+ * States the one-shot rule, because without it a cooperative caller has no reason to stop.
39
+ */
40
+ oneShot: "Agent is currently away. Your message has been received and will be read when the operator returns. " +
41
+ "This inbox accepts one message per visit — please close the session now (send with signal: wrap) instead of sending more.",
42
+ /** The session-offer answer, which names the away agent — so it is built, not fixed. */
43
+ offerFor(agentName) {
44
+ return `${agentName} is currently away. Leave a message (send with signal: wrap to close) and it will be read when they return.`;
45
+ },
46
+ };
47
+ /** The offer text with the agent name removed, so any agent's offer is recognised. */
48
+ const OFFER_SUFFIX = " is currently away. Leave a message (send with signal: wrap to close) and it will be read when they return.";
49
+ /**
50
+ * True when `text` is one of THIS daemon's away auto-replies — machine traffic, not a caller.
51
+ *
52
+ * The caller must neither answer it nor count it toward the one-shot rule. Answering produces the
53
+ * ping-pong; counting it produces the seal.
54
+ */
55
+ export function isOwnAwayAutoReply(text) {
56
+ if (text.length === 0)
57
+ return false;
58
+ if (text === AWAY_AUTO_REPLY_TEXTS.oneShot)
59
+ return true;
60
+ // The offer names an agent, so match the invariant tail — but require a non-empty name in front
61
+ // of it, so a bare quote of the suffix alone is not treated as machine traffic.
62
+ if (text.endsWith(OFFER_SUFFIX) && text.length > OFFER_SUFFIX.length)
63
+ return true;
64
+ return false;
65
+ }
66
+ //# sourceMappingURL=away-detection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"away-detection.js","sourceRoot":"","sources":["../src/away-detection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,gEAAgE;AAChE,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC;;;OAGG;IACH,OAAO,EACL,sGAAsG;QACtG,2HAA2H;IAE7H,wFAAwF;IACxF,QAAQ,CAAC,SAAiB;QACxB,OAAO,GAAG,SAAS,6GAA6G,CAAC;IACnI,CAAC;CACO,CAAC;AAEX,sFAAsF;AACtF,MAAM,YAAY,GAAG,6GAA6G,CAAC;AAEnI;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACpC,IAAI,IAAI,KAAK,qBAAqB,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IACxD,gGAAgG;IAChG,gFAAgF;IAChF,IAAI,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,IAAI,CAAC,MAAM,GAAG,YAAY,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IAClF,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -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,EAKrB,MAAM,YAAY,CAAC;AAIpB,OAAO,EAAmC,KAAK,UAAU,EAAsB,MAAM,iBAAiB,CAAC;AAEvG,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAO/D,OAAO,EAAoD,KAAK,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAiB7G,OAAO,KAAK,EAAE,mBAAmB,EAAE,iBAAiB,EAAsB,MAAM,2BAA2B,CAAC;AAS5G,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAIlE,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,2BAA2B,CAAC;AA2BnF,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAUlD,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;;;;OAIG;IACH,oBAAoB,IAAI,kBAAkB,CAAC;IAC3C;;;OAGG;IACH,iBAAiB,IAAI,eAAe,CAAC;IACrC;;OAEG;IACH,eAAe,IAAI,YAAY,CAAC;IAChC;;;;;;;;OAQG;IACH,WAAW,IAAI,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CACxC;AAYD,qBAAa,4BAA6B,YAAW,mBAAmB;IAChE,UAAU,CAAC,MAAM,EAAE,iBAAiB;CA8B3C;AAID,eAAO,MAAM,sBAAsB,QAAsB,CAAC;AAE1D;;;;;;;;;;GAUG;AACH,wBAAsB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAS7E"}
1
+ {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH,OAAO,KAAK,EACV,YAAY,EACZ,oBAAoB,EAKrB,MAAM,YAAY,CAAC;AAIpB,OAAO,EAAmC,KAAK,UAAU,EAAsB,MAAM,iBAAiB,CAAC;AAEvG,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAO/D,OAAO,EAAoD,KAAK,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAiB7G,OAAO,KAAK,EAAE,mBAAmB,EAAE,iBAAiB,EAAsB,MAAM,2BAA2B,CAAC;AAS5G,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAIlE,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,2BAA2B,CAAC;AA2BnF,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAWlD,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;;;;OAIG;IACH,oBAAoB,IAAI,kBAAkB,CAAC;IAC3C;;;OAGG;IACH,iBAAiB,IAAI,eAAe,CAAC;IACrC;;OAEG;IACH,eAAe,IAAI,YAAY,CAAC;IAChC;;;;;;;;OAQG;IACH,WAAW,IAAI,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CACxC;AAYD,qBAAa,4BAA6B,YAAW,mBAAmB;IAChE,UAAU,CAAC,MAAM,EAAE,iBAAiB;CA8B3C;AAID,eAAO,MAAM,sBAAsB,QAAsB,CAAC;AAE1D;;;;;;;;;;GAUG;AACH,wBAAsB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAS7E"}
package/dist/daemon.js CHANGED
@@ -91,6 +91,7 @@ import { startRegistryPoll } from "./registry-poll.js";
91
91
  import { CONSENT_ACCEPTED } from "./consent-migration.js";
92
92
  import { TrustSignalStore } from "./trust-signal-store.js";
93
93
  import { countAttendance, ContentTakeLedger } from "./co-attendance.js";
94
+ import { isOwnAwayAutoReply, AWAY_AUTO_REPLY_TEXTS } from "./away-detection.js";
94
95
  import { FrontierMismatchStore, renderFrontierMismatch } from "./frontier-mismatch.js";
95
96
  import { encodeCbor, decodeCbor } from "@cello-protocol/protocol-types";
96
97
  // Minimal no-op KeyProvider stub for session nodes.
@@ -870,7 +871,10 @@ async function startDaemonHoldingLock(config, singletonLock) {
870
871
  // DOD-AWAY-ACK-ONESHOT-TEXT-1 (live defect 2026-07-24): the ack must state the one-shot rule —
871
872
  // without it a cooperative caller LLM has no reason to stop, sends a follow-up, and eats the
872
873
  // DOD-INBOX-ONESHOT-1 rejection the design itself invited.
873
- const AWAY_MESSAGE_TEXT = "Agent is currently away. Your message has been received and will be read when the operator returns. This inbox accepts one message per visit — please close the session now (send with signal: wrap) instead of sending more.";
874
+ // ONE definition, shared with the detector in away-detection.ts. A second copy here is how a
875
+ // reworded away message stops being recognised as machine traffic and the mutual-seal loop
876
+ // (DOD-AWAY-MUTUAL-SEAL-1) quietly comes back.
877
+ const AWAY_MESSAGE_TEXT = AWAY_AUTO_REPLY_TEXTS.oneShot;
874
878
  // M8C-CONTACT-1: "unknown senders learn only 'dispatched' by default" — a single shared,
875
879
  // deliberately minimal template regardless of kind, distinct from AWAY-1's richer per-type text.
876
880
  const STRANGER_TEXT = "Dispatched.";
@@ -898,6 +902,30 @@ async function startDaemonHoldingLock(config, singletonLock) {
898
902
  logger.info("session.away.response.skipped_wrap", { agentName, sessionId });
899
903
  return;
900
904
  }
905
+ // DOD-AWAY-MUTUAL-SEAL-1: an away responder must not answer ANOTHER away responder, and
906
+ // must not count one toward the one-shot rule.
907
+ //
908
+ // When both agents are unattended, each side's auto-reply arrives at the other as an inbound
909
+ // message. The second such arrival looks exactly like "a caller who ignored the
910
+ // leave-a-message instruction" — so the one-shot fires on BOTH sides, each sends a [[WRAP]]
911
+ // rejection and initiates a seal, and two distinct-sender SEAL ctrl leaves is precisely what
912
+ // notarizes a session.
913
+ //
914
+ // Measured from the relay's own log 2026-08-09: sealed THREE SECONDS after opening, its
915
+ // entire content two machines telling each other nobody was home. Neither daemon learned —
916
+ // the seal completion is pushed with no pull twin — so both showed `active`, and the
917
+ // operators returned and talked into a closed room for 68 minutes before finding out.
918
+ //
919
+ // Returned silently rather than answered: a reply would continue the ping-pong, and there is
920
+ // nobody on the far side to read one.
921
+ if (isOwnAwayAutoReply(text)) {
922
+ logger.info("session.away.mutual.skipped", {
923
+ agentName,
924
+ sessionId,
925
+ impact: "no away reply and no one-shot seal — two away agents must not notarize a conversation nobody had",
926
+ });
927
+ return;
928
+ }
901
929
  }
902
930
  }
903
931
  const dedupKey = `${agentName}:${sessionId}:${kind}`;
@@ -1060,7 +1088,7 @@ async function startDaemonHoldingLock(config, singletonLock) {
1060
1088
  const isKnown = sessionNodeManager.isKnown(agentName, record.counterparty_pubkey);
1061
1089
  // DOD-AWAY-WRAP-1 AC1: request kind uses a leave-a-message greeting that names the away agent.
1062
1090
  const systemDefault = kind === "request"
1063
- ? `${agentName} is currently away. Leave a message (send with signal: wrap to close) and it will be read when they return.`
1091
+ ? AWAY_AUTO_REPLY_TEXTS.offerFor(agentName)
1064
1092
  : AWAY_MESSAGE_TEXT;
1065
1093
  awayAckSent.add(dedupKey); // guard BEFORE the async send — concurrent arrivals must not double-ack
1066
1094
  try {