@cello-protocol/daemon 0.0.197 → 0.0.199
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/agent-admin-handlers.d.ts +56 -0
- package/dist/agent-admin-handlers.d.ts.map +1 -0
- package/dist/agent-admin-handlers.js +70 -0
- package/dist/agent-admin-handlers.js.map +1 -0
- package/dist/agent-handlers.d.ts +8 -7
- package/dist/agent-handlers.d.ts.map +1 -1
- package/dist/agent-handlers.js +10 -1
- package/dist/agent-handlers.js.map +1 -1
- package/dist/agent-selection-root.d.ts +29 -0
- package/dist/agent-selection-root.d.ts.map +1 -0
- package/dist/agent-selection-root.js +126 -0
- package/dist/agent-selection-root.js.map +1 -0
- package/dist/attendance-wiring.d.ts +44 -0
- package/dist/attendance-wiring.d.ts.map +1 -0
- package/dist/attendance-wiring.js +432 -0
- package/dist/attendance-wiring.js.map +1 -0
- package/dist/backup-restore-handlers.d.ts +10 -0
- package/dist/backup-restore-handlers.d.ts.map +1 -0
- package/dist/backup-restore-handlers.js +82 -0
- package/dist/backup-restore-handlers.js.map +1 -0
- package/dist/boot-agents.d.ts +64 -0
- package/dist/boot-agents.d.ts.map +1 -0
- package/dist/boot-agents.js +241 -0
- package/dist/boot-agents.js.map +1 -0
- package/dist/boot-connection-state.d.ts +40 -0
- package/dist/boot-connection-state.d.ts.map +1 -0
- package/dist/boot-connection-state.js +136 -0
- package/dist/boot-connection-state.js.map +1 -0
- package/dist/boot-core.d.ts +36 -0
- package/dist/boot-core.d.ts.map +1 -0
- package/dist/boot-core.js +292 -0
- package/dist/boot-core.js.map +1 -0
- package/dist/boot-parked-content.d.ts +34 -0
- package/dist/boot-parked-content.d.ts.map +1 -0
- package/dist/boot-parked-content.js +527 -0
- package/dist/boot-parked-content.js.map +1 -0
- package/dist/boot-sweeps.d.ts +22 -0
- package/dist/boot-sweeps.d.ts.map +1 -0
- package/dist/boot-sweeps.js +50 -0
- package/dist/boot-sweeps.js.map +1 -0
- package/dist/connection-agents.d.ts +16 -0
- package/dist/connection-agents.d.ts.map +1 -0
- package/dist/connection-agents.js +68 -0
- package/dist/connection-agents.js.map +1 -0
- package/dist/content-park.d.ts.map +1 -1
- package/dist/content-park.js +274 -25
- package/dist/content-park.js.map +1 -1
- package/dist/daemon-handle.d.ts +57 -0
- package/dist/daemon-handle.d.ts.map +1 -0
- package/dist/daemon-handle.js +2 -0
- package/dist/daemon-handle.js.map +1 -0
- package/dist/daemon-status-report.d.ts +40 -0
- package/dist/daemon-status-report.d.ts.map +1 -0
- package/dist/daemon-status-report.js +78 -0
- package/dist/daemon-status-report.js.map +1 -0
- package/dist/daemon.d.ts +5 -66
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +332 -4894
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-open-registry.d.ts +1 -1
- package/dist/delivery-open-registry.js +1 -1
- package/dist/directory-connect.d.ts +27 -0
- package/dist/directory-connect.d.ts.map +1 -0
- package/dist/directory-connect.js +98 -0
- package/dist/directory-connect.js.map +1 -0
- package/dist/disconnect-cleanup.d.ts +45 -0
- package/dist/disconnect-cleanup.d.ts.map +1 -0
- package/dist/disconnect-cleanup.js +65 -0
- package/dist/disconnect-cleanup.js.map +1 -0
- package/dist/document-surface.d.ts +32 -0
- package/dist/document-surface.d.ts.map +1 -0
- package/dist/document-surface.js +150 -0
- package/dist/document-surface.js.map +1 -0
- package/dist/document-wiring.d.ts +53 -0
- package/dist/document-wiring.d.ts.map +1 -0
- package/dist/document-wiring.js +323 -0
- package/dist/document-wiring.js.map +1 -0
- package/dist/inbound-sessions.d.ts +9 -1
- package/dist/inbound-sessions.d.ts.map +1 -1
- package/dist/inbound-sessions.js +9 -1
- package/dist/inbound-sessions.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/ipc-surface.d.ts +44 -0
- package/dist/ipc-surface.d.ts.map +1 -0
- package/dist/ipc-surface.js +106 -0
- package/dist/ipc-surface.js.map +1 -0
- package/dist/network-directory-node.d.ts +0 -18
- package/dist/network-directory-node.d.ts.map +1 -1
- package/dist/network-directory-node.js +1 -38
- package/dist/network-directory-node.js.map +1 -1
- package/dist/notification-handlers.d.ts.map +1 -1
- package/dist/notification-handlers.js +2 -0
- package/dist/notification-handlers.js.map +1 -1
- package/dist/onboarding-guidance.d.ts +79 -0
- package/dist/onboarding-guidance.d.ts.map +1 -0
- package/dist/onboarding-guidance.js +95 -0
- package/dist/onboarding-guidance.js.map +1 -0
- package/dist/operator-guidance.d.ts +25 -0
- package/dist/operator-guidance.d.ts.map +1 -0
- package/dist/operator-guidance.js +50 -0
- package/dist/operator-guidance.js.map +1 -0
- package/dist/park-recovery.d.ts +22 -1
- package/dist/park-recovery.d.ts.map +1 -1
- package/dist/park-recovery.js.map +1 -1
- package/dist/park-refusals.d.ts +147 -0
- package/dist/park-refusals.d.ts.map +1 -0
- package/dist/park-refusals.js +331 -0
- package/dist/park-refusals.js.map +1 -0
- package/dist/refusal-notices.d.ts.map +1 -1
- package/dist/refusal-notices.js +22 -2
- package/dist/refusal-notices.js.map +1 -1
- package/dist/refusal-reasons.d.ts +13 -0
- package/dist/refusal-reasons.d.ts.map +1 -1
- package/dist/refusal-reasons.js +19 -0
- package/dist/refusal-reasons.js.map +1 -1
- package/dist/register-handler.d.ts +8 -7
- package/dist/register-handler.d.ts.map +1 -1
- package/dist/register-handler.js.map +1 -1
- package/dist/session-content-handlers.d.ts.map +1 -1
- package/dist/session-content-handlers.js +2 -0
- package/dist/session-content-handlers.js.map +1 -1
- package/dist/session-content-ingest.d.ts +1 -0
- package/dist/session-content-ingest.d.ts.map +1 -1
- package/dist/session-content-ingest.js +2 -2
- package/dist/session-content-ingest.js.map +1 -1
- package/dist/session-node-factory.d.ts +18 -0
- package/dist/session-node-factory.d.ts.map +1 -0
- package/dist/session-node-factory.js +182 -0
- package/dist/session-node-factory.js.map +1 -0
- package/dist/session-node-manager.d.ts +1 -0
- package/dist/session-node-manager.d.ts.map +1 -1
- package/dist/session-node-manager.js.map +1 -1
- package/dist/session-node-types.d.ts +8 -0
- package/dist/session-node-types.d.ts.map +1 -1
- package/dist/session-node-types.js.map +1 -1
- package/dist/session-notify.d.ts +46 -0
- package/dist/session-notify.d.ts.map +1 -0
- package/dist/session-notify.js +115 -0
- package/dist/session-notify.js.map +1 -0
- package/dist/session-read-handlers.d.ts.map +1 -1
- package/dist/session-read-handlers.js +0 -1
- package/dist/session-read-handlers.js.map +1 -1
- package/dist/session-salt-agreement.d.ts +0 -7
- package/dist/session-salt-agreement.d.ts.map +1 -1
- package/dist/session-salt-agreement.js +0 -7
- package/dist/session-salt-agreement.js.map +1 -1
- package/dist/session-views.d.ts +46 -0
- package/dist/session-views.d.ts.map +1 -0
- package/dist/session-views.js +197 -0
- package/dist/session-views.js.map +1 -0
- package/dist/signal-handlers.d.ts +66 -0
- package/dist/signal-handlers.d.ts.map +1 -0
- package/dist/signal-handlers.js +975 -0
- package/dist/signal-handlers.js.map +1 -0
- package/dist/signaling-wiring.d.ts +100 -0
- package/dist/signaling-wiring.d.ts.map +1 -0
- package/dist/signaling-wiring.js +392 -0
- package/dist/signaling-wiring.js.map +1 -0
- package/dist/start-agent.d.ts +64 -0
- package/dist/start-agent.d.ts.map +1 -0
- package/dist/start-agent.js +136 -0
- package/dist/start-agent.js.map +1 -0
- package/dist/status-handler.d.ts +46 -0
- package/dist/status-handler.d.ts.map +1 -0
- package/dist/status-handler.js +58 -0
- package/dist/status-handler.js.map +1 -0
- package/dist/test-handlers.d.ts +62 -0
- package/dist/test-handlers.d.ts.map +1 -0
- package/dist/test-handlers.js +239 -0
- package/dist/test-handlers.js.map +1 -0
- package/dist/types.d.ts +0 -11
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -12
- package/dist/types.js.map +1 -1
- package/dist/unresolved-nodes-report.d.ts +14 -0
- package/dist/unresolved-nodes-report.d.ts.map +1 -0
- package/dist/unresolved-nodes-report.js +96 -0
- package/dist/unresolved-nodes-report.js.map +1 -0
- package/dist/who-resolver.d.ts +15 -0
- package/dist/who-resolver.d.ts.map +1 -0
- package/dist/who-resolver.js +46 -0
- package/dist/who-resolver.js.map +1 -0
- package/dist/wire-content-hash.d.ts +0 -2
- package/dist/wire-content-hash.d.ts.map +1 -1
- package/dist/wire-content-hash.js +0 -4
- package/dist/wire-content-hash.js.map +1 -1
- package/package.json +5 -5
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AgentInfo, AgentState } from "./types.js";
|
|
2
|
+
import type { SessionNodeManager } from "./session-node-manager.js";
|
|
3
|
+
export interface ConnectionAgentsDeps {
|
|
4
|
+
/** Live containers — copies would report a stale roster and a stale online set forever. */
|
|
5
|
+
agents: ReadonlyArray<AgentInfo>;
|
|
6
|
+
perConnectionState: ReadonlyMap<string, {
|
|
7
|
+
currentAgent: string | null;
|
|
8
|
+
}>;
|
|
9
|
+
onlineAgents: ReadonlySet<string>;
|
|
10
|
+
sessionNodeManager: SessionNodeManager;
|
|
11
|
+
agentStateFor: (a: AgentInfo) => AgentState;
|
|
12
|
+
}
|
|
13
|
+
export declare function createConnectionAgents(deps: ConnectionAgentsDeps): {
|
|
14
|
+
getAgentsForConnection: (connectionId: string) => AgentInfo[];
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=connection-agents.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection-agents.d.ts","sourceRoot":"","sources":["../src/connection-agents.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAEpE,MAAM,WAAW,oBAAoB;IACnC,2FAA2F;IAC3F,MAAM,EAAE,aAAa,CAAC,SAAS,CAAC,CAAC;IACjC,kBAAkB,EAAE,WAAW,CAAC,MAAM,EAAE;QAAE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IACzE,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAClC,kBAAkB,EAAE,kBAAkB,CAAC;IACvC,aAAa,EAAE,CAAC,CAAC,EAAE,SAAS,KAAK,UAAU,CAAC;CAC7C;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,oBAAoB;2CAIjB,MAAM,KAAG,SAAS,EAAE;EAyDnE"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent list from ONE connection's point of view.
|
|
3
|
+
*
|
|
4
|
+
* Per-connection on purpose: which agent a connection has selected changes what it is shown, and
|
|
5
|
+
* collapsing that into a daemon-wide list is how an operator ends up reading another connection's
|
|
6
|
+
* state as their own.
|
|
7
|
+
*/
|
|
8
|
+
import { countAttendance } from "./co-attendance.js";
|
|
9
|
+
export function createConnectionAgents(deps) {
|
|
10
|
+
const { agents, perConnectionState, onlineAgents, sessionNodeManager, agentStateFor } = deps;
|
|
11
|
+
// Build agent list from this connection's perspective
|
|
12
|
+
function getAgentsForConnection(connectionId) {
|
|
13
|
+
const connState = perConnectionState.get(connectionId);
|
|
14
|
+
const currentAgent = connState?.currentAgent ?? null;
|
|
15
|
+
return agents
|
|
16
|
+
.filter((a) => a.state !== "load_failed")
|
|
17
|
+
.map((a) => {
|
|
18
|
+
// `state` reports READINESS only; selection is a SEPARATE `selected` flag. Never fold
|
|
19
|
+
// selection into `state` — a selected agent is not at a different level of readiness than a
|
|
20
|
+
// second healthy online agent. (This is why `current` was dropped from the enum.)
|
|
21
|
+
const state = agentStateFor(a);
|
|
22
|
+
const selected = onlineAgents.has(a.name) && a.name === currentAgent;
|
|
23
|
+
return {
|
|
24
|
+
name: a.name,
|
|
25
|
+
state,
|
|
26
|
+
/**
|
|
27
|
+
* `selected` IS THIS CONNECTION'S VIEW, NOT THE AGENT'S — `DOD-M15-IPCVISIBLE-1` clause 3.
|
|
28
|
+
*
|
|
29
|
+
* Every `cello` CLI invocation opens a FRESH connection, which starts with no current
|
|
30
|
+
* agent. So a client asking about its own state through the CLI always reads `false`, for
|
|
31
|
+
* an agent it genuinely has selected in another session. Both Andre and a Hermes agent
|
|
32
|
+
* misread it that way during one investigation, in opposite directions.
|
|
33
|
+
*
|
|
34
|
+
* The field name cannot be changed without breaking every reader, so it is ANNOTATED: the
|
|
35
|
+
* sibling below says whose view this is, and `attended_by` says how many connections hold
|
|
36
|
+
* this agent at all — which is the question people were actually asking.
|
|
37
|
+
*/
|
|
38
|
+
selected,
|
|
39
|
+
selected_by_this_connection: selected,
|
|
40
|
+
attended_by: countAttendance(perConnectionState, a.name),
|
|
41
|
+
pubkey: a.pubkey,
|
|
42
|
+
// M8B F14 (fix 5): per-agent standing-receiver readiness on the MCP surface
|
|
43
|
+
// (cello_status / cello_list_agents), so a deaf agent is visible to the operator.
|
|
44
|
+
standing_receiver_ready: sessionNodeManager.getStandingReceiverReady(a.name),
|
|
45
|
+
standing_receiver_reachability: sessionNodeManager.getStandingReceiverReachability(a.name),
|
|
46
|
+
/**
|
|
47
|
+
* DOD-M15-RELAYSLOTS-1: WHY it is not reachable, and what to do about it.
|
|
48
|
+
*
|
|
49
|
+
* `standing_receiver_reachability` says `retrying` or `unreachable` and stops there, which
|
|
50
|
+
* for the person reading it is indistinguishable from the product being broken. The relay
|
|
51
|
+
* now refuses for reasons someone can act on — no token from a directory yet, too many
|
|
52
|
+
* sessions still open, this relay is misconfigured — each with a different next step, and
|
|
53
|
+
* every one of them is wasted if it only reaches a log file. Absent when the last attempt
|
|
54
|
+
* succeeded.
|
|
55
|
+
*/
|
|
56
|
+
...(sessionNodeManager.getStandingReceiverRefusal(a.name)
|
|
57
|
+
? { standing_receiver_refusal: sessionNodeManager.getStandingReceiverRefusal(a.name) }
|
|
58
|
+
: {}),
|
|
59
|
+
// DOD-COATTEND-VISIBLE-1 AC2: how many sessions are driving this agent, including this
|
|
60
|
+
// one. Live, not a high-water mark — it drops when a session disconnects. `selected` says
|
|
61
|
+
// whether YOU hold it; this says whether anyone else does too.
|
|
62
|
+
attendance: countAttendance(perConnectionState, a.name),
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return { getAgentsForConnection };
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=connection-agents.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection-agents.js","sourceRoot":"","sources":["../src/connection-agents.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAarD,MAAM,UAAU,sBAAsB,CAAC,IAA0B;IAC/D,MAAM,EAAE,MAAM,EAAE,kBAAkB,EAAE,YAAY,EAAE,kBAAkB,EAAE,aAAa,EAAE,GAAG,IAAI,CAAC;IAE7F,sDAAsD;IACtD,SAAS,sBAAsB,CAAC,YAAoB;QAClD,MAAM,SAAS,GAAG,kBAAkB,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;QACvD,MAAM,YAAY,GAAG,SAAS,EAAE,YAAY,IAAI,IAAI,CAAC;QAErD,OAAO,MAAM;aACV,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,aAAa,CAAC;aACxC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;YACT,sFAAsF;YACtF,4FAA4F;YAC5F,kFAAkF;YAClF,MAAM,KAAK,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC;YAC/B,MAAM,QAAQ,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,KAAK,YAAY,CAAC;YACrE,OAAO;gBACL,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,KAAK;gBACL;;;;;;;;;;;mBAWG;gBACH,QAAQ;gBACR,2BAA2B,EAAE,QAAQ;gBACrC,WAAW,EAAE,eAAe,CAAC,kBAAkB,EAAE,CAAC,CAAC,IAAI,CAAC;gBACxD,MAAM,EAAE,CAAC,CAAC,MAAM;gBAChB,4EAA4E;gBAC5E,kFAAkF;gBAClF,uBAAuB,EAAE,kBAAkB,CAAC,wBAAwB,CAAC,CAAC,CAAC,IAAI,CAAC;gBAC5E,8BAA8B,EAAE,kBAAkB,CAAC,+BAA+B,CAAC,CAAC,CAAC,IAAI,CAAC;gBAC1F;;;;;;;;;mBASG;gBACH,GAAG,CAAC,kBAAkB,CAAC,0BAA0B,CAAC,CAAC,CAAC,IAAI,CAAC;oBACvD,CAAC,CAAC,EAAE,yBAAyB,EAAE,kBAAkB,CAAC,0BAA0B,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE;oBACtF,CAAC,CAAC,EAAE,CAAC;gBACP,uFAAuF;gBACvF,0FAA0F;gBAC1F,+DAA+D;gBAC/D,UAAU,EAAE,eAAe,CAAC,kBAAkB,EAAE,CAAC,CAAC,IAAI,CAAC;aACxD,CAAC;QACJ,CAAC,CAAC,CAAC;IACP,CAAC;IAED,OAAO,EAAE,sBAAsB,EAAE,CAAC;AACpC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"content-park.d.ts","sourceRoot":"","sources":["../src/content-park.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AACrE,OAAO,EAAE,iBAAiB,EAAwD,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"content-park.d.ts","sourceRoot":"","sources":["../src/content-park.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AACrE,OAAO,EAAE,iBAAiB,EAAwD,MAAM,0BAA0B,CAAC;AAYnH,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,kBAAkB,EAAE,kBAAkB,CAAC;IACvC,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,cAAc,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,WAAW,GAAG,SAAS,CAAC;IAC/D;;;;;OAKG;IACH,eAAe,EAAE,qBAAqB,CAAC;IACvC;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,CAAC,IAAI,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,EAAE,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,iBAAiB,CAAC;CACpH;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,eAAe;qCAkzBP,MAAM,uBAA4B,OAAO,CAAC,IAAI,CAAC;iCAkEzD,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,KAAG,IAAI;6CAxsBhD;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,eACpC,MAAM,cACP,MAAM,EAAE,KACnB,OAAO,CACN;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,KAAK,CAAC;YAAE,WAAW,EAAE,MAAM,CAAC;YAAC,SAAS,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE,GAG7I;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,CACnD;EAgjCF"}
|
package/dist/content-park.js
CHANGED
|
@@ -24,9 +24,45 @@ import { ContentParkClient, ContentParkRefusedError, ContentParkUnreachableError
|
|
|
24
24
|
import { extractErrorMessage } from "./error-message.js";
|
|
25
25
|
import { decodeParkEnvelope, sealParkEnvelope } from "./park-envelope.js";
|
|
26
26
|
import { contentHashFor, resolveContentHashAlg, isKnownContentHashAlg, CONTENT_HASH_ALGS } from "./wire-content-hash.js";
|
|
27
|
+
import { PARK_REFUSAL_REASONS, PARK_REFUSAL_NOTICE, TERMINAL_SESSION_STATUSES, } from "./park-refusals.js";
|
|
27
28
|
export function createContentPark(deps) {
|
|
28
29
|
const { logger, sessionNodeManager, agents, getKeyProvider, securityGateway } = deps;
|
|
29
30
|
const newParkClient = deps.makeContentParkClient ?? ((o) => new ContentParkClient(o));
|
|
31
|
+
/**
|
|
32
|
+
* `041-PARKSTUCK` Unit 2 — **THE PARK DRAIN'S REFUSALS REACH A PERSON.**
|
|
33
|
+
*
|
|
34
|
+
* Every branch below already wrote a `reason`, an `impact` and a `guidance`, at ERROR, addressed
|
|
35
|
+
* to nobody: the drain runs unattended, its return value goes to an IPC caller that in production
|
|
36
|
+
* is `autoRecoverForAgent`, and that only tallies counts into a log line. So the operator's whole
|
|
37
|
+
* account of a message that never leaves the mailbox was the INGEST reason recorded one layer up
|
|
38
|
+
* — `session_committed`, which is where the message was turned away on arrival, not why it is
|
|
39
|
+
* stuck. Naming the exit point instead of the cause is the substitution this project's debugging
|
|
40
|
+
* discipline is written against, and pointed at an operator it is a dead end: there is no next
|
|
41
|
+
* hop, and `daemon.log` is not an affordance.
|
|
42
|
+
*
|
|
43
|
+
* ⚠️ **BOTH REASONS REACH THE INBOX, and neither replaces the other.** They answer different
|
|
44
|
+
* questions — one says the conversation is closed, this one says why the message cannot leave —
|
|
45
|
+
* and the notice store keys on (session, reason), so they sit side by side rather than
|
|
46
|
+
* overwriting.
|
|
47
|
+
*/
|
|
48
|
+
/**
|
|
49
|
+
* ⚠️ **IT RETURNS THE REFUSAL RECORD, AND THAT IS THE ENFORCEMENT — review H3, second half.**
|
|
50
|
+
*
|
|
51
|
+
* The first version only wrote the notice, and the caller pushed its own `{ reason }` object
|
|
52
|
+
* separately. So a future branch could push a reason and forget to note it, and the enforcer
|
|
53
|
+
* could not see the difference: its scan asked only whether the reason NAME appeared somewhere in
|
|
54
|
+
* the file, which a bare `refusals.push({ reason })` satisfies on its own.
|
|
55
|
+
*
|
|
56
|
+
* Constructing the record HERE makes the pairing structural rather than remembered — the only way
|
|
57
|
+
* to get a park reason into the drain's refusal list is to have already told the operator about
|
|
58
|
+
* it. `dod-m15-inboxcause-1.test.ts` then has something it can actually check.
|
|
59
|
+
*/
|
|
60
|
+
function noteParkRefusal(agentName, sessionId, contentHash, reason, ctx) {
|
|
61
|
+
// `noteContentRefusal` does not throw — a persistence failure logs and falls back to memory —
|
|
62
|
+
// so this needs no guard of its own, and adding one would hide the ERROR it already emits.
|
|
63
|
+
sessionNodeManager.noteContentRefusal(agentName, sessionId, reason, PARK_REFUSAL_NOTICE[reason](ctx));
|
|
64
|
+
return { contentHash, sessionId, reason };
|
|
65
|
+
}
|
|
30
66
|
// MSG-001-3b: content-park deposit/pull IPC handlers. These drive the daemon's
|
|
31
67
|
// ContentParkClient directly so the daemon↔relay store-and-forward transport can be
|
|
32
68
|
// proven (J-CONTENT increment 1) before the send/receive-path integration. The relay
|
|
@@ -269,6 +305,16 @@ export function createContentPark(deps) {
|
|
|
269
305
|
// That exact confusion was injected and the test goes red on it.
|
|
270
306
|
let annexed = false;
|
|
271
307
|
let screenedOut = false;
|
|
308
|
+
/**
|
|
309
|
+
* Did the terminal-screen branch actually KEEP the bytes? — verification NEW-2, which is
|
|
310
|
+
* H1's defect one branch over and in the code H1 was written for.
|
|
311
|
+
*
|
|
312
|
+
* `quarantineRefusedInbound` answers `null` on the same four reachable paths, and this
|
|
313
|
+
* branch confirm-deleted the relay copy regardless while its notice said "It is KEPT as
|
|
314
|
+
* evidence" unconditionally. Declared out here rather than inside the `try` so the delete
|
|
315
|
+
* and the notice can both read it.
|
|
316
|
+
*/
|
|
317
|
+
let screenRetained = false;
|
|
272
318
|
let screenDeferred = false;
|
|
273
319
|
/**
|
|
274
320
|
* WHY the entry is stuck, for the caller — review B2a F5.
|
|
@@ -280,8 +326,44 @@ export function createContentPark(deps) {
|
|
|
280
326
|
* tamper — was one of the four.
|
|
281
327
|
*/
|
|
282
328
|
let annexRefusal = null;
|
|
329
|
+
/** The thrown message, carried into the operator's notice rather than left in the log. */
|
|
330
|
+
let annexErrorDetail = null;
|
|
331
|
+
/**
|
|
332
|
+
* `041-PARKSTUCK` — CAN THIS EVER BE CHECKED AGAIN, or is the answer fixed for the rest of
|
|
333
|
+
* time? Two different states, and only one of them is worth retrying.
|
|
334
|
+
*
|
|
335
|
+
* The status is read HERE rather than inferred from `ingest.reason`, because
|
|
336
|
+
* `session_committed` covers THREE statuses and one of them — `seal_interrupted_pending` —
|
|
337
|
+
* is documented as explicitly non-terminal. Collapsing them would release a message on a
|
|
338
|
+
* session still moving toward a notarization.
|
|
339
|
+
*
|
|
340
|
+
* `null` when the record cannot be read, and that is NOT treated as terminal: an
|
|
341
|
+
* unreadable record is "we do not know", and releasing on it would delete the only
|
|
342
|
+
* surviving copy of a message on an assumption. Fail-closed costs another drain.
|
|
343
|
+
*/
|
|
344
|
+
const sessionRecord = sessionNodeManager.getSessionRecord(recipientAgent.name, e.sessionIdHex);
|
|
345
|
+
const sessionStatus = sessionRecord?.status ?? null;
|
|
346
|
+
const sessionTerminal = sessionStatus !== null && TERMINAL_SESSION_STATUSES.has(sessionStatus);
|
|
347
|
+
let saltReason = null;
|
|
348
|
+
let declaredAlgSeen = "(absent)";
|
|
349
|
+
/**
|
|
350
|
+
* ⚠️ **WHICH STEP THREW — review M4, and without it one label spoke for four.**
|
|
351
|
+
*
|
|
352
|
+
* The `try` below opens at the decode and does not close until after the screen, the
|
|
353
|
+
* quarantine and the annex write, so a throw from ANY of them was reported as
|
|
354
|
+
* `annex_decode_failed`. This unit then promoted that label to an operator notice asserting
|
|
355
|
+
* *"a wire or version difference, not a claim about the sender"* and sending them to a log
|
|
356
|
+
* event that had not fired. It also contradicted the sibling comment below, which promises
|
|
357
|
+
* `annex_write_failed` is *"only what it says — the annex write ran and failed"*.
|
|
358
|
+
*
|
|
359
|
+
* And the mislabel is the common case, not the rare one: `decodeParkEnvelope` already
|
|
360
|
+
* succeeded on these exact bytes in `recoverParkedEntry` before ingest returned
|
|
361
|
+
* `session_committed`, so a genuine decode failure here is close to unreachable.
|
|
362
|
+
*/
|
|
363
|
+
let decoded = false;
|
|
283
364
|
try {
|
|
284
365
|
const env = decodeParkEnvelope(unsealed);
|
|
366
|
+
decoded = true;
|
|
285
367
|
/**
|
|
286
368
|
* ⚠️ THE SECOND CONTENT-HASH VERIFIER — `DOD-M15-SEALWIRE-1` part B1 review F3, closed by
|
|
287
369
|
* part B2a.
|
|
@@ -315,9 +397,21 @@ export function createContentPark(deps) {
|
|
|
315
397
|
* null salt is the DEFAULT for exactly the sessions whose content arrives this way.
|
|
316
398
|
*/
|
|
317
399
|
const alg = resolveContentHashAlg(env.contentHashAlg);
|
|
318
|
-
|
|
319
|
-
|
|
400
|
+
declaredAlgSeen = env.contentHashAlg ?? "(absent)";
|
|
401
|
+
/**
|
|
402
|
+
* `getSessionContentSaltState`, NOT `getSessionContentSalt` — `041-PARKSTUCK` Unit 2.
|
|
403
|
+
*
|
|
404
|
+
* A bare `null` answers "no salt was ever agreed" and "a salt row is here and this
|
|
405
|
+
* machine cannot read it" identically, and those send the operator to opposite places:
|
|
406
|
+
* one to the conversation, the other to their own disk. The hashing path is unchanged —
|
|
407
|
+
* this reads the same salt through the same method, and only the REASON is new.
|
|
408
|
+
*/
|
|
409
|
+
const saltState = alg.ok
|
|
410
|
+
? sessionNodeManager.getSessionContentSaltState(recipientAgent.name, e.sessionIdHex)
|
|
320
411
|
: null;
|
|
412
|
+
const sessionSalt = saltState?.salt ?? null;
|
|
413
|
+
if (saltState !== null && saltState.salt === null)
|
|
414
|
+
saltReason = saltState.reason;
|
|
321
415
|
let computed = null;
|
|
322
416
|
// Discriminated BEFORE the call rather than recovered from a thrown string, so the two
|
|
323
417
|
// causes are branches rather than message-matching.
|
|
@@ -326,12 +420,12 @@ export function createContentPark(deps) {
|
|
|
326
420
|
// A name this build cannot read. NOT a hash mismatch: there is no value to compare
|
|
327
421
|
// against, and reporting it as one would be a tamper claim for a version difference —
|
|
328
422
|
// the same substitution `DOD-M15-SEALWIRE-1` part B1 removed on the direct path.
|
|
329
|
-
algFailure = { reason:
|
|
423
|
+
algFailure = { reason: PARK_REFUSAL_REASONS.ANNEX_ALG_UNKNOWN, detail: `the sender named "${alg.value}"` };
|
|
330
424
|
}
|
|
331
425
|
else if (alg.alg !== CONTENT_HASH_ALGS.SHA256 && !sessionSalt) {
|
|
332
426
|
algFailure = {
|
|
333
|
-
reason:
|
|
334
|
-
detail: `the sender used ${alg.alg} and this side holds no salt for the session`,
|
|
427
|
+
reason: PARK_REFUSAL_REASONS.ANNEX_SALT_UNAVAILABLE,
|
|
428
|
+
detail: `the sender used ${alg.alg} and this side holds no salt for the session (${saltReason ?? "unknown"})`,
|
|
335
429
|
};
|
|
336
430
|
}
|
|
337
431
|
else {
|
|
@@ -346,32 +440,42 @@ export function createContentPark(deps) {
|
|
|
346
440
|
catch (err) {
|
|
347
441
|
// Unreachable given the two checks above; kept so a future algorithm that throws for a
|
|
348
442
|
// third reason cannot fall through into the mismatch branch and be called a tamper.
|
|
349
|
-
algFailure = { reason:
|
|
443
|
+
algFailure = { reason: PARK_REFUSAL_REASONS.ANNEX_ALG_UNKNOWN, detail: extractErrorMessage(err) };
|
|
350
444
|
}
|
|
351
445
|
}
|
|
352
446
|
if (algFailure !== null) {
|
|
353
447
|
annexRefusal = algFailure.reason;
|
|
354
|
-
logger.error(algFailure.reason ===
|
|
448
|
+
logger.error(algFailure.reason === PARK_REFUSAL_REASONS.ANNEX_SALT_UNAVAILABLE
|
|
355
449
|
? "content.recover.annex.salt_unavailable"
|
|
356
450
|
: "content.recover.annex.alg_unknown", {
|
|
357
451
|
sessionId: e.sessionIdHex, contentHash: e.contentHashHex, agentName: recipientAgent.name,
|
|
358
452
|
declaredAlg: env.contentHashAlg ?? "(absent)",
|
|
359
453
|
detail: algFailure.detail,
|
|
360
|
-
|
|
361
|
-
|
|
454
|
+
sessionStatus: sessionStatus ?? "(unreadable)",
|
|
455
|
+
impact: "this parked message could not be CHECKED — not that it failed a check. It was not annexed. Nothing here says the sender did anything wrong.",
|
|
456
|
+
guidance: algFailure.reason === PARK_REFUSAL_REASONS.ANNEX_SALT_UNAVAILABLE
|
|
362
457
|
// Lifted from the direct path's equivalent, because the operator needs the same
|
|
363
458
|
// answer wherever the message arrived. Deliberately does NOT promise delivery:
|
|
364
459
|
// without the salt this entry cannot be verified on any future drain either.
|
|
365
460
|
// DOD-M15-SALTSPLIT-1 review MEDIUM-3: "never completed" is now one of TWO ways to
|
|
366
461
|
// hold no salt. The discard undoes an agreement that DID complete, so an operator
|
|
367
462
|
// reading the old sentence would look for a failure that never happened.
|
|
368
|
-
|
|
463
|
+
//
|
|
464
|
+
// ⚠️ **THE LAST SENTENCE USED TO BE "close it and start a new one" — `041-PARKSTUCK`.**
|
|
465
|
+
// It ran 731 times over 64 hours against a session that was ALREADY closed, and
|
|
466
|
+
// closing it is what made the refusal permanent: the ingest side then refuses
|
|
467
|
+
// `session_committed` on every drain. Naming an action the reader took three days
|
|
468
|
+
// ago is worse than saying nothing. What replaces it branches on the status,
|
|
469
|
+
// which this line has always had and never read.
|
|
470
|
+
? `This side holds no salt for the session, for one of two reasons. If session.salt.discarded is in the log, the agreement completed and this side then dropped its salt on purpose because the counterparty said it could never hold one. Otherwise the agreement never completed — look for session.salt.read.failed or session.salt.persist.failed. Either way their build is NOT the problem; do not ask them to upgrade. ${sessionTerminal
|
|
471
|
+
? `This session is "${sessionStatus}" and cannot agree a salt ever again, so this message can never be checked — the relay copy is being released so it stops being re-pulled.`
|
|
472
|
+
: "This message stays on the relay and is re-checked on every drain; a salt is agreed while both sides are connected, so staying connected is what clears it."}`
|
|
369
473
|
: "Their CELLO build is newer than this one: ask which version they run, and upgrade. The message stays on the relay and is delivered once this daemon can read that algorithm.",
|
|
370
474
|
correlationId,
|
|
371
475
|
});
|
|
372
476
|
}
|
|
373
477
|
else if (Buffer.from(computed).toString("hex") !== e.contentHashHex) {
|
|
374
|
-
annexRefusal =
|
|
478
|
+
annexRefusal = PARK_REFUSAL_REASONS.ANNEX_HASH_MISMATCH;
|
|
375
479
|
logger.error("content.recover.annex.hash_mismatch", {
|
|
376
480
|
sessionId: e.sessionIdHex, contentHash: e.contentHashHex, agentName: recipientAgent.name,
|
|
377
481
|
// The algorithm the comparison RAN UNDER. Without it a mismatch is unfalsifiable from
|
|
@@ -418,6 +522,7 @@ export function createContentPark(deps) {
|
|
|
418
522
|
* closed.
|
|
419
523
|
*/
|
|
420
524
|
const kept = sessionNodeManager.quarantineRefusedInbound(recipientAgent.name, e.sessionIdHex, verdict.reason ?? "inbound_screen_blocked", env.content, e.contentHashHex, env.senderPubkey ? Buffer.from(env.senderPubkey).toString("hex") : null, correlationId);
|
|
525
|
+
screenRetained = kept !== null;
|
|
421
526
|
logger.warn("content.recover.annex.screened_out", {
|
|
422
527
|
sessionId: e.sessionIdHex, contentHash: e.contentHashHex, agentName: recipientAgent.name,
|
|
423
528
|
retained: kept !== null,
|
|
@@ -440,11 +545,16 @@ export function createContentPark(deps) {
|
|
|
440
545
|
}
|
|
441
546
|
}
|
|
442
547
|
catch (err) {
|
|
443
|
-
annexRefusal =
|
|
444
|
-
|
|
548
|
+
annexRefusal = decoded
|
|
549
|
+
? PARK_REFUSAL_REASONS.ANNEX_WRITE_FAILED
|
|
550
|
+
: PARK_REFUSAL_REASONS.ANNEX_DECODE_FAILED;
|
|
551
|
+
annexErrorDetail = extractErrorMessage(err);
|
|
552
|
+
logger.error(decoded ? "content.recover.annex.write_failed" : "content.recover.annex.decode_failed", {
|
|
445
553
|
sessionId: e.sessionIdHex, contentHash: e.contentHashHex,
|
|
446
|
-
impact:
|
|
447
|
-
|
|
554
|
+
impact: decoded
|
|
555
|
+
? "the envelope decoded and a later step threw — screening, retention or the annex write. NOT annexed, relay copy kept."
|
|
556
|
+
: "envelope could not be decoded — NOT annexed, relay copy kept",
|
|
557
|
+
error: annexErrorDetail, correlationId,
|
|
448
558
|
});
|
|
449
559
|
}
|
|
450
560
|
if (annexed) {
|
|
@@ -464,26 +574,165 @@ export function createContentPark(deps) {
|
|
|
464
574
|
}
|
|
465
575
|
}
|
|
466
576
|
else if (screenedOut) {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
577
|
+
/**
|
|
578
|
+
* Terminal block: identical bytes are rejected identically forever, so the relay copy goes
|
|
579
|
+
* or the re-pull loop returns.
|
|
580
|
+
*
|
|
581
|
+
* ⚠️ **AND ONLY IF THE BYTES WERE ACTUALLY KEPT — verification NEW-2.** This deleted
|
|
582
|
+
* unconditionally while asserting "retained as quarantined evidence above", which is H1's
|
|
583
|
+
* defect reproduced in the branch H1 was written for. Screener-blocked content aimed at a
|
|
584
|
+
* closed conversation is the highest-value evidence in the product; losing it because the
|
|
585
|
+
* quarantine was full is the one outcome worse than it arriving again.
|
|
586
|
+
*/
|
|
587
|
+
let screenReleased = false;
|
|
588
|
+
if (screenRetained) {
|
|
589
|
+
try {
|
|
590
|
+
await client.confirm(node, Buffer.from(recipientPubkey, "hex"), contentHashBytes, kp);
|
|
591
|
+
screenReleased = true;
|
|
592
|
+
}
|
|
593
|
+
catch (err) {
|
|
594
|
+
logger.warn("content.recover.confirm.failed", { sessionId: e.sessionIdHex, contentHash: e.contentHashHex, error: extractErrorMessage(err) });
|
|
595
|
+
}
|
|
470
596
|
}
|
|
471
|
-
|
|
472
|
-
logger.warn("content.recover.
|
|
597
|
+
else {
|
|
598
|
+
logger.warn("content.recover.release.withheld", {
|
|
599
|
+
sessionId: e.sessionIdHex, contentHash: e.contentHashHex, agentName: recipientAgent.name,
|
|
600
|
+
reason: PARK_REFUSAL_REASONS.ANNEX_SCREENED_OUT,
|
|
601
|
+
impact: "screener-blocked content could NOT be retained locally — see session.content.quarantine.skipped or .failed above — so the relay copy is KEPT rather than deleted. It will arrive and be blocked again, which is the lesser harm: deleting it would destroy the only record that this was ever sent.",
|
|
602
|
+
guidance: "Free space for this conversation and the copy is retained on a later drain. cello_quarantined shows what it is already holding.",
|
|
603
|
+
correlationId,
|
|
604
|
+
});
|
|
473
605
|
}
|
|
606
|
+
/**
|
|
607
|
+
* ⚠️ **THIS BRANCH TOLD THE OPERATOR NOTHING — review M9.**
|
|
608
|
+
*
|
|
609
|
+
* It is the only park branch that DELETES the relay's copy on purpose, and it is the one
|
|
610
|
+
* with the strongest claim on an operator's attention: content rejected outright by the
|
|
611
|
+
* screener, aimed at a conversation that has already closed. Its `impact` field was
|
|
612
|
+
* written for a person and read by nobody, which is the pattern this whole unit exists to
|
|
613
|
+
* end — and leaving it here while fixing the neighbouring branches would have been the
|
|
614
|
+
* class fixed one instance at a time.
|
|
615
|
+
*
|
|
616
|
+
* Deliberately NOT pushed into `refusals`: that list is what the drain reports as STUCK,
|
|
617
|
+
* and this entry is resolved — retained, and gone from the relay. The operator surface is
|
|
618
|
+
* the part that was missing, not the stuck-list entry.
|
|
619
|
+
*/
|
|
620
|
+
noteParkRefusal(recipientAgent.name, e.sessionIdHex, e.contentHashHex, PARK_REFUSAL_REASONS.ANNEX_SCREENED_OUT, { sessionStatus, released: screenReleased, retained: screenRetained, declaredAlg: declaredAlgSeen, saltReason, errorDetail: null });
|
|
474
621
|
}
|
|
475
622
|
else if (screenDeferred) {
|
|
476
|
-
refusals.push(
|
|
623
|
+
refusals.push(noteParkRefusal(recipientAgent.name, e.sessionIdHex, e.contentHashHex, PARK_REFUSAL_REASONS.ANNEX_SCREEN_UNAVAILABLE, { sessionStatus, released: false, declaredAlg: declaredAlgSeen, saltReason, errorDetail: null }));
|
|
477
624
|
}
|
|
478
625
|
else {
|
|
479
626
|
// Keep the relay copy — it is now the only one. Review F7: name why the entry is STUCK,
|
|
480
627
|
// not why ingest refused it. `annexRefusal` carries the branch that actually stopped it;
|
|
481
628
|
// the bare `annex_write_failed` fallback is now only what it says — the annex write ran
|
|
482
629
|
// and failed.
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
630
|
+
const stuckReason = annexRefusal ?? PARK_REFUSAL_REASONS.ANNEX_WRITE_FAILED;
|
|
631
|
+
/**
|
|
632
|
+
* ─── `041-PARKSTUCK` Unit 1: THE EXIT THE SAFETY RULE DID NOT HAVE ────────────────────
|
|
633
|
+
*
|
|
634
|
+
* *"Never drop a message we could not verify"* is right, and it had no case for
|
|
635
|
+
* verification being IMPOSSIBLE FOR THE REST OF TIME rather than merely failing now.
|
|
636
|
+
* Every input to this decision is then immutable and the code re-derives the same answer
|
|
637
|
+
* forever: measured at 731 refusals in 64 hours on one message, roughly one every five
|
|
638
|
+
* minutes, with the operator never shown it once.
|
|
639
|
+
*
|
|
640
|
+
* All three conditions must hold, and the middle one is why this cannot widen:
|
|
641
|
+
* 1. the session is TERMINAL (`sealed` / `abandoned`) — no salt can ever be agreed for
|
|
642
|
+
* it again, because agreement runs while both sides are connected;
|
|
643
|
+
* 2. the sender named a SALTED algorithm, so a salt is required to check the bytes;
|
|
644
|
+
* 3. this side holds no usable salt.
|
|
645
|
+
*
|
|
646
|
+
* ⚠️ EVERY OTHER REFUSAL IN THIS BRANCH STILL KEEPS THE RELAY COPY, deliberately. A
|
|
647
|
+
* screener that is down comes back; a build that cannot compute an algorithm gets
|
|
648
|
+
* upgraded; a decode failure is a version difference. Those answers CHANGE, so releasing
|
|
649
|
+
* one drain early would lose a message that would have gone through — which is strictly
|
|
650
|
+
* worse than the loop.
|
|
651
|
+
*
|
|
652
|
+
* ⚠️ **AND THE LOCAL COPY IS PROVEN, NEVER ASSUMED — review H1.**
|
|
653
|
+
*
|
|
654
|
+
* This comment used to assert that `ingestReceivedContent`'s `session_committed` exit
|
|
655
|
+
* *had* quarantined the bytes, and gate on nothing. That claim is true on the ordinary
|
|
656
|
+
* path and false on four reachable ones: no database, the conversation's byte budget
|
|
657
|
+
* already spent, the row not stored, and a throwing write — each logging, in its own
|
|
658
|
+
* words, that *nothing holds a copy of it*. The budget case is not exotic; it is the
|
|
659
|
+
* ordinary shape of a long conversation that then ends.
|
|
660
|
+
*
|
|
661
|
+
* Deleting the relay's copy on the strength of an unchecked retention is delete-on-
|
|
662
|
+
* best-effort, which the comment forty lines above forbids by name: *"annex FIRST,
|
|
663
|
+
* confirm-delete SECOND, and only if the annex committed… delete-on-best-effort converts
|
|
664
|
+
* the loop into PERMANENT SILENT loss — strictly worse than the bug."* So `retained` is
|
|
665
|
+
* now a fact carried out of ingest, and a failed retention keeps the relay copy. The loop
|
|
666
|
+
* is the lesser harm and it is loud while it lasts.
|
|
667
|
+
*/
|
|
668
|
+
/**
|
|
669
|
+
* ⚠️ **`saltReason === "none"` IS THE THIRD CONDITION, and leaving it out was a way to
|
|
670
|
+
* destroy a message — review H2.**
|
|
671
|
+
*
|
|
672
|
+
* `getSessionContentSaltState` answers `unreadable` for THREE situations and only one of
|
|
673
|
+
* them is permanent: the database is not open (a shutdown or startup race), the read
|
|
674
|
+
* THREW, or the stored blob is the wrong width (real corruption). `getSessionSalt` also
|
|
675
|
+
* returns null on a throwing read. So a SQLCipher lock or an IO blip on one drain makes a
|
|
676
|
+
* perfectly good salt look absent — and without this clause the next line deletes the
|
|
677
|
+
* relay's last copy of a message the FOLLOWING drain would have annexed.
|
|
678
|
+
*
|
|
679
|
+
* That is the order's explicit prohibition: a release one drain too early loses a message
|
|
680
|
+
* that would have gone through. `none` is the only answer that cannot change back: no
|
|
681
|
+
* salt row exists, and on a terminal session none can ever be written.
|
|
682
|
+
*
|
|
683
|
+
* The live `dcec3c3f…` case is `none` — there is not one `session.salt.*` event for that
|
|
684
|
+
* session — so the loop this unit exists to stop still closes.
|
|
685
|
+
*/
|
|
686
|
+
const releasable = stuckReason === PARK_REFUSAL_REASONS.ANNEX_SALT_UNAVAILABLE &&
|
|
687
|
+
sessionTerminal &&
|
|
688
|
+
saltReason === "none" &&
|
|
689
|
+
ingest.retained === true;
|
|
690
|
+
if (!releasable && stuckReason === PARK_REFUSAL_REASONS.ANNEX_SALT_UNAVAILABLE && sessionTerminal && saltReason === "none") {
|
|
691
|
+
/**
|
|
692
|
+
* The loop continues, and the reason it continues is a LOCAL storage failure rather than
|
|
693
|
+
* anything about this message. Said out loud because otherwise this is indistinguishable
|
|
694
|
+
* from the defect this unit fixed — the same refusal, at the same cadence, forever.
|
|
695
|
+
*/
|
|
696
|
+
logger.warn("content.recover.release.withheld", {
|
|
697
|
+
sessionId: e.sessionIdHex, contentHash: e.contentHashHex, agentName: recipientAgent.name,
|
|
698
|
+
impact: "this message can never be checked and the relay copy would normally be dropped, but this daemon could NOT retain a local copy of it — see session.content.quarantine.skipped or .failed above. The relay copy is kept instead, so it will keep being pulled and refused, because deleting it would leave the message nowhere at all.",
|
|
699
|
+
guidance: "Free space for this conversation and the copy is retained on a later drain, after which the loop stops on its own. A conversation that has spent its storage budget cannot retain more; cello_quarantined shows what it is already holding.",
|
|
700
|
+
correlationId,
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* ⚠️ **REPORTED ON THE SUCCESS PATH — the notice is written AFTER the delete, never
|
|
705
|
+
* before it.** `released` is what the operator's sentence turns on ("it is now gone" vs
|
|
706
|
+
* "the relay still holds it"), and a confirm can fail. Deciding the wording from the
|
|
707
|
+
* INTENT to release rather than from the release having happened is how a notice comes to
|
|
708
|
+
* state a conclusion nothing has reached yet.
|
|
709
|
+
*/
|
|
710
|
+
let released = false;
|
|
711
|
+
if (releasable) {
|
|
712
|
+
try {
|
|
713
|
+
await client.confirm(node, Buffer.from(recipientPubkey, "hex"), contentHashBytes, kp);
|
|
714
|
+
released = true;
|
|
715
|
+
logger.warn("content.recover.released", {
|
|
716
|
+
sessionId: e.sessionIdHex, contentHash: e.contentHashHex, agentName: recipientAgent.name,
|
|
717
|
+
reason: stuckReason, sessionStatus, saltReason: saltReason ?? "(unknown)",
|
|
718
|
+
impact: "this message can NEVER be checked — the conversation is closed, so no salt can ever be agreed for it — and the relay's copy has been deleted so it stops being pulled and refused on every drain. The content itself is retained locally as quarantined evidence and is readable with cello_quarantined.",
|
|
719
|
+
guidance: "Nothing to retry and nothing to repair. If the message mattered, ask the sender OUT OF BAND to say it again in a NEW conversation.",
|
|
720
|
+
correlationId,
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
catch (err) {
|
|
724
|
+
// The relay copy survives, so the loop continues for now and nothing is lost. Named
|
|
725
|
+
// separately from `content.recover.confirm.failed` because on THIS branch the failed
|
|
726
|
+
// delete is the whole reason the entry is still there.
|
|
727
|
+
logger.warn("content.recover.release.failed", {
|
|
728
|
+
sessionId: e.sessionIdHex, contentHash: e.contentHashHex, agentName: recipientAgent.name,
|
|
729
|
+
error: extractErrorMessage(err),
|
|
730
|
+
impact: "the relay copy could not be deleted, so this message will be pulled and refused again on the next drain. Nothing is lost; the loop simply has not stopped yet.",
|
|
731
|
+
correlationId,
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
refusals.push(noteParkRefusal(recipientAgent.name, e.sessionIdHex, e.contentHashHex, stuckReason, { sessionStatus, released, declaredAlg: declaredAlgSeen, saltReason, errorDetail: annexErrorDetail }));
|
|
487
736
|
}
|
|
488
737
|
}
|
|
489
738
|
else {
|