Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"scripts": {
"build": "node scripts/build.mjs",
"build:packages": "turbo build",
"build:runtime:packages": "pnpm -r --filter @origintrail-official/dkg-core --filter @origintrail-official/dkg-storage --filter @origintrail-official/dkg-query --filter @origintrail-official/dkg-publisher --filter @origintrail-official/dkg-chain --filter @origintrail-official/dkg-epcis --filter @origintrail-official/dkg-okf --filter @origintrail-official/dkg-random-sampling --filter @origintrail-official/dkg-agent --filter @origintrail-official/dkg-graph-viz --filter @origintrail-official/dkg-node-ui --filter @origintrail-official/dkg-adapter-openclaw --filter @origintrail-official/dkg-adapter-hermes --filter @origintrail-official/kafka-plugin --filter @origintrail-official/dkg run build",
"build:runtime:packages": "pnpm -r --filter @origintrail-official/dkg-core... --filter @origintrail-official/dkg-storage... --filter @origintrail-official/dkg-query... --filter @origintrail-official/dkg-publisher... --filter @origintrail-official/dkg-chain... --filter @origintrail-official/dkg-epcis... --filter @origintrail-official/dkg-okf... --filter @origintrail-official/dkg-random-sampling... --filter @origintrail-official/dkg-agent... --filter @origintrail-official/dkg-graph-viz... --filter @origintrail-official/dkg-node-ui... --filter @origintrail-official/dkg-adapter-openclaw... --filter @origintrail-official/dkg-adapter-hermes... --filter @origintrail-official/kafka-plugin... --filter @origintrail-official/dkg... run build",
"build:runtime": "pnpm run build:runtime:packages && pnpm --filter @origintrail-official/dkg-node-ui run build:ui",
"test": "turbo test && pnpm run test:scripts",
"test:scripts": "node --test scripts/lib/__tests__/*.test.mjs",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@
("a\rb", '"a\\rb"'),
("a\fb", '"a\\fb"'),
("a\bb", '"a\\bb"'),
# remaining ASCII controls -> UCHAR escapes (#416)
("nul\u0000x", '"nul\\u0000x"'),
("vt\u000bx", '"vt\\u000Bx"'),
("us\u001fx", '"us\\u001Fx"'),
("del\u007fx", '"del\\u007Fx"'),
]


Expand Down
5 changes: 2 additions & 3 deletions packages/agent/src/dkg-agent-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -891,9 +891,8 @@ export class DKGAgentBase {
protected hostModeReconcilerTimer: ReturnType<typeof setInterval> | null = null;
protected hostModePruneTimer: ReturnType<typeof setInterval> | null = null;
// rc.9 PR-10: joinApprovalRetryQueue + joinApprovalRetryTimer
// deleted. The substrate's SQLite-backed ProtocolOutbox + its tick
// (`Messenger.processOutboxTick`) + opportunistic on-connect flush
// (`Messenger.processOutboxOnConnect`) replace the entire in-memory
// deleted. The substrate's SQLite-backed ProtocolOutbox + its
// scheduled tick (`Messenger.processOutboxTick`) replace the in-memory
// queue: persistence across restart, generic per-protocol coverage,
// identical backoff-ladder semantics. Operator-facing diagnostics
// (`listPendingJoinApprovalRetries`) are stubbed to [] until PR-12
Expand Down
21 changes: 6 additions & 15 deletions packages/agent/src/dkg-agent-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,27 +172,18 @@ export const STORAGE_ACK_REGISTRATION_RETRY_MS = 30_000;
* the local curator state is correct but the invitee never learns to
* sync, and their own retries can't help because they don't yet hold the
* delegation that would let private-sync auth succeed. The tick walks the
* queue's `due()` entries with exponential backoff. Opportunistic retries
* also fire from `connection:open` when the invitee's peer reconnects,
* which usually wins the race; the timer is the safety net for cases
* where reconnect events are missed (e.g. relayed reconnects that don't
* surface a fresh `connection:open` on the curator).
* queue's `due()` entries with exponential backoff. This separate join queue
* retains its own peer lifecycle policy; Universal Messenger rows below are
* scheduled-only.
*/
export const JOIN_APPROVAL_RETRY_TICK_MS = 30_000;

/**
* Tick interval for the chat outbox retry queue. Same 30s cadence as
* the join-approval queue (`JOIN_APPROVAL_RETRY_TICK_MS`). The cadence
* doesn't gate the FIRST retry — a backoff-due entry that's been
* waiting since 5s after first failure may sit idle for up to 25s
* before this tick picks it up — but the dominant retry trigger in
* practice is the `connection:open` opportunistic flush
* (`processMessageOutboxOnConnect`), which fires the moment the
* recipient peer becomes reachable again. The tick is the safety net
* for cases where reconnect events are missed (e.g. relayed reconnects
* that don't surface a fresh `connection:open` on the sender) or
* where the recipient was reachable all along but transport failures
* are coming from somewhere upstream of libp2p.
* is the sole automatic trigger: reconnect churn must not bypass a row's
* persisted `nextAttemptAt`. A due entry may sit for up to one tick interval
* before the scheduler picks it up.
*/
export const MESSAGE_OUTBOX_TICK_MS = 30_000;

Expand Down
27 changes: 11 additions & 16 deletions packages/agent/src/dkg-agent-join.ts
Original file line number Diff line number Diff line change
Expand Up @@ -716,8 +716,7 @@ export class JoinRequestMethods extends DKGAgentBase {
ctx,
`join-approval for "${contextGraphId}" → ${agentAddress} not delivered now ` +
`(error=${result.error ?? 'unknown'}). Curator-local state is correct; ` +
`substrate outbox holds the queued send and will retry on its backoff ` +
`ladder + on the invitee's next reconnect.`,
`substrate outbox holds the queued send and will retry on its backoff ladder.`,
);
}

Expand All @@ -727,8 +726,8 @@ export class JoinRequestMethods extends DKGAgentBase {
* delivery state matters.
*
* Used by:
* * The substrate's periodic outbox tick + on-connect flush —
* both transparent to this call (rc.9 PR-10).
* * The substrate's periodic outbox tick, transparent to this call
* (rc.9 PR-10).
* * The operator-facing route `POST /api/context-graph/{id}/redeliver-approval`,
* which lets an operator (or peer agent via the chat MCP) re-poke
* the curator when the automated retry isn't fast enough.
Expand Down Expand Up @@ -856,17 +855,16 @@ export class JoinRequestMethods extends DKGAgentBase {
*/
// rc.9 PR-10: processJoinApprovalRetryQueueTick +
// processJoinApprovalRetryQueueOnConnect deleted. The substrate's
// Messenger.processOutboxTick + Messenger.processOutboxOnConnect
// cover /dkg/10.0.1/join-request automatically (same as chat in
// Messenger.processOutboxTick covers /dkg/10.0.1/join-request
// automatically (same as chat in
// PR-3), so the two dedicated processors are obsolete. Operator
// re-fire route POST /api/context-graph/{id}/redeliver-approval is
// unchanged — it still calls redeliverJoinApproval which now
// simply re-issues the substrate send.

/**
* Re-attempt delivery of a single chat outbox entry. Centralised so
* the periodic tick + the connection:open opportunistic flush share
* one code path. Returns the entry's current state so the caller can
* Re-attempt delivery of a single chat outbox entry from the periodic
* scheduler. Returns the entry's current state so the caller can
* decide what to log.
*
* Goes through `messageHandler.sendChat` directly (bypassing
Expand All @@ -884,17 +882,14 @@ export class JoinRequestMethods extends DKGAgentBase {
* class: an inbound circuit connection from P was open and live, but
* every `libp2p.dialProtocol(P, ...)` retry on our side failed with
* "The dial request has no valid addresses for peer" for several
* minutes. Daemon logs showed 31 `connection:open` events from P
* (all inbound, all via R) + 20 opportunistic-flush attempts, all
* failing dialProtocol — and then the moment ONE outbound connection
* succeeded (which populated peerStore from outbound identify), the
* very next opportunistic-flush delivered the queued message.
* minutes. Reverse-path enrichment ensures the next scheduled retry sees a
* usable address without letting connection churn bypass persisted backoff.
*
* The clean fix would be inside libp2p (`dialProtocol` should reuse
* an existing open connection of any direction — see PR 5 in the
* postmortem follow-up plan), but until that lands, populating
* peerStore from the inbound circuit's address gives the dialer
* something to find on the very next attempt.
* something to find on the next scheduled attempt.
*
* Public so a unit test can exercise it directly without standing up
* a full libp2p network (the listener that calls it is registered
Expand Down Expand Up @@ -1140,7 +1135,7 @@ export class JoinRequestMethods extends DKGAgentBase {
ctx,
`${label} for "${contextGraphId}" to ${agentAddress} (${targetPeerId}) ` +
`queued in substrate outbox: ${sendResult.error}. ` +
`Substrate will retry on its own backoff ladder + on the invitee's next reconnect.`,
`Substrate will retry on its persisted backoff schedule.`,
);
return { delivered: false, peerId: targetPeerId, error: sendResult.error };
}
Expand Down
36 changes: 4 additions & 32 deletions packages/agent/src/dkg-agent-lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,8 @@ import {
resolveSyncResponderSnapshotBudgetOptions,
} from './sync/responder/sync-handler.js';
import { runSyncOnConnect, SyncOnConnectPostSyncError, type SyncOnConnectOutcome, type SyncOnConnectPeerOutcome } from './sync/on-connect/sync-on-connect.js';
import { mapWithConcurrency, CATCHUP_MAX_CONCURRENT_PEER_SYNCS } from './sync/map-with-concurrency.js';
import { mapWithConcurrency } from './map-with-concurrency.js';
import { CATCHUP_MAX_CONCURRENT_PEER_SYNCS } from './sync/catchup-concurrency.js';
import {
getSyncBackpressureSnapshot,
getSyncBackpressureBusyError,
Expand Down Expand Up @@ -2345,14 +2346,8 @@ export class LifecycleSyncMethods extends DKGAgentBase {
this.node.libp2p.addEventListener('connection:open', (evt) => {
const remotePeer = evt.detail.remotePeer.toString();
if (remotePeer === this.node.libp2p.peerId.toString()) return;
// rc.9 PR-10: the dedicated join-approval on-connect flush is
// gone. The substrate's `Messenger.processOutboxOnConnect` (a
// few lines further down in this handler) now covers join-
// approved retries too, since /dkg/10.0.1/join-request is now
// a substrate-managed protocol.

// Reverse-path peerStore enrichment for inbound circuit-relay
// connections, then the symmetric chat-outbox flush.
// connections.
//
// Closes the "Window D" class from the May 2026 Miles↔Lex 6h
// soak postmortem: an inbound circuit connection from peer P
Expand All @@ -2365,17 +2360,6 @@ export class LifecycleSyncMethods extends DKGAgentBase {
// dialProtocol find an address and try it.
//
// User review on PR #536 caught the original ordering bug:
// running enrichment and the outbox flush in parallel
// fire-and-forget meant the first flush attempt could
// still hit `dialProtocol` against an EMPTY peerStore and
// fail with the same "no valid addresses" error this PR is
// meant to heal — pushing recovery onto the next 30s tick
// or another reconnect. Sequence the two: await enrichment
// first, then flush. Both stay wrapped in their own
// try/catch so an enrichment failure logs a warning and
// still lets the outbox flush proceed (it might succeed
// anyway via a stale-but-usable cached path).
//
// The whole chain runs as a fire-and-forget IIFE so the
// listener itself doesn't await — libp2p's
// `connection:open` emitter is synchronous and we don't
Expand All @@ -2396,17 +2380,6 @@ export class LifecycleSyncMethods extends DKGAgentBase {
const message = err instanceof Error ? err.message : String(err);
this.log.warn(ctx, `Reverse-path peerStore enrichment failed for ${remotePeer}: ${message}`);
}
// Universal Messenger substrate (rc.9 PR-2/PR-3): drain
// the generic outbox for this peer. Replaces the rc.8
// chat-specific outbox flush — the substrate now carries
// chat (PR-3) and will carry every other short-message
// protocol after PR-8..PR-11.
try {
await this.messenger.processOutboxOnConnect(remotePeer);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
this.log.warn(ctx, `Opportunistic Messenger-outbox retry on connect failed for ${remotePeer}: ${message}`);
}
// PR-2 (SWM-fanout plan): drain pending sender-key packages
// that were queued because the recipient had no advertised
// peerId at publish time. Tolerant of profile-lookup failure
Expand Down Expand Up @@ -2604,8 +2577,7 @@ export class LifecycleSyncMethods extends DKGAgentBase {
// MESSAGE_OUTBOX_TICK_MS for the rationale (silent-drop on
// transport failure used to lose operator-typed messages from
// `dkg_send_message`; this is the safety-net retry loop that turns
// them into eventual successes, complemented by the
// opportunistic-on-reconnect path in the connection:open listener).
// them into eventual successes on their persisted retry schedule.
// Universal Messenger substrate retry tick (rc.9 PR-2 +
// PR-3). The rc.8 chat-specific tick was deleted in PR-3;
// this is now the only outbox tick — chat (PR-3) and every
Expand Down
Loading
Loading