@cello-protocol/daemon 0.0.171 → 0.0.172

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 CHANGED
@@ -29,6 +29,7 @@ import { mkdir } from "node:fs/promises";
29
29
  import { randomUUID } from "node:crypto";
30
30
  import { dirname, join } from "node:path";
31
31
  import { loadAgents } from "./agent-loader.js";
32
+ import { RestartSealResolver } from "./restart-seal-resolver.js";
32
33
  import { acquireLock, removeLockIfOwned } from "./lock-file.js";
33
34
  import { acquireSingletonLock } from "./singleton-lock.js";
34
35
  import { createIpcServer } from "./ipc-server.js";
@@ -276,6 +277,8 @@ async function startDaemonHoldingLock(config, singletonLock) {
276
277
  // classification (INV-TYPE-CARRY). Absent pubkey = polling disabled, all types unclassified.
277
278
  const typeRegistry = new TypeRegistry();
278
279
  let stopRegistryPoll;
280
+ // DOD-M12B-RESTART-SEAL-1: assigned as the last act of boot, stopped in stop().
281
+ let restartSealResolver;
279
282
  if (config.registryPubkey && config.registryPollScheduler) {
280
283
  const registryVersionStore = new DbRegistryVersionStore(sessionNodeManager.getDb(), logger);
281
284
  stopRegistryPoll = startRegistryPoll({
@@ -4003,6 +4006,11 @@ async function startDaemonHoldingLock(config, singletonLock) {
4003
4006
  if (config.registryPollScheduler) {
4004
4007
  config.registryPollScheduler.cancel();
4005
4008
  }
4009
+ // DOD-M12B-RESTART-SEAL-1: stop opening directory ceremonies. A seal is the most outbound thing
4010
+ // this daemon does, and DOD-M12B-SHUTDOWN-1's rule is that a shutdown which keeps starting new
4011
+ // outbound work is not draining. Above the `daemon.stopped` log with the other cancels, because
4012
+ // this is "stop making new work", not "tear down transports".
4013
+ await restartSealResolver?.stop();
4006
4014
  logger.info("daemon.stopped", { pid: process.pid, reason });
4007
4015
  // DOD-LOGOUT-EXIT-1: what the teardown actually DID, carried to onStopped so the binary can
4008
4016
  // exit non-zero on a dirty stop. Without it a shutdown that threw halfway — sessions never
@@ -4101,6 +4109,57 @@ async function startDaemonHoldingLock(config, singletonLock) {
4101
4109
  // M8C-TGDOOR-1: cold-capable — start the poller if a token was already configured from a
4102
4110
  // prior run, without waiting for any agent to come online or any client to attach.
4103
4111
  startTelegramPollerIfConfigured();
4112
+ // DOD-M12B-RESTART-SEAL-1: seal the sessions the LAST shutdown orphaned.
4113
+ //
4114
+ // 114 of 118 interrupted sessions on one operator's machine were flipped by our own shutdown
4115
+ // sweep and then sat there, because nothing has ever moved a session out of `interrupted`. Their
4116
+ // only exit was a force-abandon, which forfeits the notarized receipt.
4117
+ //
4118
+ // Started last, like the Telegram poller, and resolved LAZILY out of the live `handlers` map: the
4119
+ // close handler is registered far earlier, but reading it inside the callback is what lets a test
4120
+ // swap it after boot and prove this wiring exists rather than assume it.
4121
+ restartSealResolver = new RestartSealResolver({
4122
+ logger,
4123
+ listRestartOrphans: () => sessionNodeManager.listRestartOrphanedSessions(),
4124
+ ...(config.restartSealInitialDelayMs !== undefined
4125
+ ? { initialDelayMs: config.restartSealInitialDelayMs }
4126
+ : {}),
4127
+ ...(config.restartSealStaggerMs !== undefined ? { staggerMs: config.restartSealStaggerMs } : {}),
4128
+ sealSession: async (agentName, sessionId) => {
4129
+ const close = handlers.get("cello_close_session");
4130
+ if (!close)
4131
+ return { ok: false, reason: "close_handler_missing" };
4132
+ const res = (await close({ session_id: sessionId, agent: agentName }, `restart-seal-${sessionId}`));
4133
+ // SUCCESS IS A RECEIPT, NOT AN `ok`. The interrupted close answers `ok: true` for a bilateral
4134
+ // COMMITMENT that nobody has notarized — `seal_interrupted_pending`, the status this resolver
4135
+ // exists to stop producing. Reading `ok` as success logged "resolved" for 137 sessions that
4136
+ // got no receipt at all. `sealed_root` is present only when a notarization actually happened.
4137
+ if (typeof res?.["sealed_root"] === "string" && res["sealed_root"].length > 0) {
4138
+ return { ok: true };
4139
+ }
4140
+ // Everything the close computed about WHY, carried instead of collapsed. `reason` is an exit
4141
+ // point — `seal_interrupted_rejected_by_counterparty` alone stands for six distinct causes,
4142
+ // and the close already put the discriminating detail on the response.
4143
+ const detail = {};
4144
+ for (const k of ["rejection_reason", "your_leaf_count", "their_leaf_count", "diverging_leaf_index",
4145
+ "seal_pending_reason", "seal_receipt", "missing_leaves", "held_messages", "status"]) {
4146
+ if (res?.[k] !== undefined)
4147
+ detail[k] = res[k];
4148
+ }
4149
+ const retry = res?.["retry_after_seconds"];
4150
+ return {
4151
+ ok: false,
4152
+ reason: (typeof res?.["seal_pending_reason"] === "string" ? res["seal_pending_reason"] : undefined)
4153
+ ?? (typeof res?.["reason"] === "string" ? res["reason"] : undefined)
4154
+ ?? "close_returned_no_receipt",
4155
+ ...(typeof retry === "number" ? { retryAfterSeconds: retry } : {}),
4156
+ ...(typeof res?.["guidance"] === "string" ? { guidance: res["guidance"] } : {}),
4157
+ ...(Object.keys(detail).length > 0 ? { detail } : {}),
4158
+ };
4159
+ },
4160
+ markGaveUp: (agentName, sessionId, reason) => sessionNodeManager.markRestartSealGaveUp(agentName, sessionId, reason),
4161
+ });
4162
+ restartSealResolver.start();
4104
4163
  /**
4105
4164
  * The live handler map.
4106
4165
  *