@adhdev/daemon-core 0.9.82-rc.256 → 0.9.82-rc.258
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/boot/daemon-lifecycle.d.ts +3 -0
- package/dist/index.js +287 -43
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +287 -43
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events-coordinator.d.ts +22 -0
- package/dist/mesh/mesh-events-pending.d.ts +7 -0
- package/dist/mesh/mesh-events-utils.d.ts +2 -0
- package/dist/mesh/mesh-events.d.ts +2 -2
- package/dist/mesh/mesh-ledger.d.ts +1 -1
- package/dist/mesh/mesh-routing.d.ts +70 -0
- package/dist/mesh/mesh-runtime-store.d.ts +3 -0
- package/package.json +1 -1
- package/src/boot/daemon-lifecycle.ts +8 -0
- package/src/commands/router.ts +12 -4
- package/src/mesh/mesh-events-coordinator.ts +110 -54
- package/src/mesh/mesh-events-pending.ts +57 -3
- package/src/mesh/mesh-events-utils.ts +20 -0
- package/src/mesh/mesh-events.ts +2 -0
- package/src/mesh/mesh-ledger.ts +1 -0
- package/src/mesh/mesh-routing.ts +272 -0
- package/src/mesh/mesh-runtime-store.ts +37 -0
package/src/mesh/mesh-events.ts
CHANGED
|
@@ -11,6 +11,7 @@ export {
|
|
|
11
11
|
drainPendingMeshCoordinatorEvents,
|
|
12
12
|
getPendingMeshCoordinatorEvents,
|
|
13
13
|
clearPendingMeshCoordinatorEvents,
|
|
14
|
+
markMeshCoordinatorEventDirectDelivered,
|
|
14
15
|
} from './mesh-events-pending.js';
|
|
15
16
|
|
|
16
17
|
export {
|
|
@@ -25,4 +26,5 @@ export {
|
|
|
25
26
|
setupMeshEventForwarding,
|
|
26
27
|
isMeshCoordinatorEvent,
|
|
27
28
|
__resetIdleAutoFastForwardForTests,
|
|
29
|
+
__resetMeshWorkspaceCacheForTests,
|
|
28
30
|
} from './mesh-events-coordinator.js';
|
package/src/mesh/mesh-ledger.ts
CHANGED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import type { DaemonComponents } from '../boot/daemon-lifecycle.js';
|
|
2
|
+
import { getActiveDirectDispatches } from './mesh-work-queue.js';
|
|
3
|
+
import { hasUnterminalDirectDispatchLedgerEntry } from './mesh-events-stale.js';
|
|
4
|
+
import { appendLedgerEntry, readLedgerEntries } from './mesh-ledger.js';
|
|
5
|
+
import { LOG } from '../logging/logger.js';
|
|
6
|
+
import { readNonEmptyString } from './mesh-events-utils.js';
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// R1: single-source coordinator routing resolution
|
|
10
|
+
//
|
|
11
|
+
// Worker→node→mesh→coordinator routing used to be reconstructed inline inside
|
|
12
|
+
// setupMeshEventForwarding by reading six settings "stamp" fields
|
|
13
|
+
// (meshNodeFor / meshNodeId / meshCoordinatorFor / meshCoordinatorDaemonId /
|
|
14
|
+
// meshCoordinatorNodeId / launchedByCoordinator) plus two ledger queries plus a
|
|
15
|
+
// workspace fallback, scattered across ~50 lines. Any missing stamp silently
|
|
16
|
+
// changed the routing decision (delegate dropped, mesh unresolved) — design
|
|
17
|
+
// vulnerability #2.
|
|
18
|
+
//
|
|
19
|
+
// resolveWorkerDelegateRouting() is the ONE place that interprets those inputs.
|
|
20
|
+
// The stamps are inputs *internal to this function*; no external branch reads
|
|
21
|
+
// them to make a routing decision. The forwarder consumes the typed result only.
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
export type WorkerDelegateRejectionReason =
|
|
25
|
+
| 'not_cli'
|
|
26
|
+
| 'no_workspace'
|
|
27
|
+
| 'no_worker_envelope'
|
|
28
|
+
| 'coordinator_not_dispatch_target'
|
|
29
|
+
| 'mesh_unresolved';
|
|
30
|
+
|
|
31
|
+
export interface WorkerDelegateRouting {
|
|
32
|
+
/** True when the source session is a mesh worker whose events must route to a coordinator. */
|
|
33
|
+
isDelegate: boolean;
|
|
34
|
+
/** Resolved mesh id (runtime stamp, direct-dispatch recovery, or workspace lookup). */
|
|
35
|
+
meshId: string;
|
|
36
|
+
/** Resolved node id within the mesh, or '' when it cannot be determined. */
|
|
37
|
+
nodeId: string;
|
|
38
|
+
/** Human-readable node label for the coordinator system message. */
|
|
39
|
+
nodeLabel: string;
|
|
40
|
+
/** Coordinator daemon anchor (prefixed id) when the worker carries one; '' otherwise. */
|
|
41
|
+
coordinatorDaemonId: string;
|
|
42
|
+
/** Source session workspace (best-effort; '' when the session/workspace is missing). */
|
|
43
|
+
workspace: string;
|
|
44
|
+
/** Source session id echoed back, so diagnostics can name the dropped event's origin. */
|
|
45
|
+
sessionId: string;
|
|
46
|
+
/** Why routing was rejected (isDelegate=false), for fail-loud diagnostics (R4). */
|
|
47
|
+
rejectionReason?: WorkerDelegateRejectionReason;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface ResolveDeps {
|
|
51
|
+
/** Resolve a mesh by id (local config + inline cache). */
|
|
52
|
+
getMeshById: (meshId: string) => any | undefined;
|
|
53
|
+
/** Resolve a mesh by workspace path (cached repo lookup). */
|
|
54
|
+
getMeshByWorkspace: (workspace: string) => any | undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readSettings(state: any): Record<string, unknown> {
|
|
58
|
+
return state?.settings && typeof state.settings === 'object'
|
|
59
|
+
? state.settings as Record<string, unknown>
|
|
60
|
+
: {};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolve how a source CLI session's mesh events should route to a coordinator.
|
|
65
|
+
*
|
|
66
|
+
* This is the single authoritative interpretation of the worker envelope. It folds
|
|
67
|
+
* together every signal that previously lived in inline branches:
|
|
68
|
+
* - runtime stamps (meshNodeFor / meshNodeId)
|
|
69
|
+
* - the coordinator anchor (meshCoordinatorDaemonId) and launch marker (launchedByCoordinator,
|
|
70
|
+
* meshCoordinatorNodeId) — any one proves delegation even if the node/mesh stamp was dropped
|
|
71
|
+
* - direct-dispatch recovery: a coordinator session that is itself a dispatch target
|
|
72
|
+
* - workspace→mesh fallback when no runtime mesh id survived
|
|
73
|
+
*
|
|
74
|
+
* Returns isDelegate=false (with a rejectionReason) instead of throwing, so the caller
|
|
75
|
+
* can either ignore non-delegate events or surface a diagnostic.
|
|
76
|
+
*/
|
|
77
|
+
export function resolveWorkerDelegateRouting(
|
|
78
|
+
components: DaemonComponents,
|
|
79
|
+
instanceId: string,
|
|
80
|
+
deps: ResolveDeps,
|
|
81
|
+
): WorkerDelegateRouting {
|
|
82
|
+
const sessionId = readNonEmptyString(instanceId);
|
|
83
|
+
let workspace = '';
|
|
84
|
+
let coordinatorDaemonId = '';
|
|
85
|
+
const reject = (rejectionReason: WorkerDelegateRejectionReason): WorkerDelegateRouting => ({
|
|
86
|
+
isDelegate: false,
|
|
87
|
+
meshId: '',
|
|
88
|
+
nodeId: '',
|
|
89
|
+
nodeLabel: '',
|
|
90
|
+
coordinatorDaemonId,
|
|
91
|
+
workspace,
|
|
92
|
+
sessionId,
|
|
93
|
+
rejectionReason,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const sourceInstance = components.instanceManager.getInstance(instanceId);
|
|
97
|
+
if (!sourceInstance || sourceInstance.category !== 'cli') return reject('not_cli');
|
|
98
|
+
|
|
99
|
+
const state = sourceInstance.getState();
|
|
100
|
+
workspace = readNonEmptyString(state.workspace);
|
|
101
|
+
if (!workspace) return reject('no_workspace');
|
|
102
|
+
|
|
103
|
+
const settings = readSettings(state);
|
|
104
|
+
coordinatorDaemonId = readNonEmptyString(settings.meshCoordinatorDaemonId);
|
|
105
|
+
|
|
106
|
+
// A coordinator session (meshCoordinatorFor set) is only treated as a worker delegate
|
|
107
|
+
// when it is itself the target of an active direct dispatch — otherwise its own events
|
|
108
|
+
// must not be routed back to a coordinator (it IS the coordinator).
|
|
109
|
+
const coordinatorMeshId = readNonEmptyString(settings.meshCoordinatorFor);
|
|
110
|
+
let meshIdFromDirectDispatch = '';
|
|
111
|
+
if (coordinatorMeshId) {
|
|
112
|
+
let hasActiveDispatch = false;
|
|
113
|
+
try {
|
|
114
|
+
hasActiveDispatch =
|
|
115
|
+
getActiveDirectDispatches(coordinatorMeshId).some(d => d.sessionId === instanceId)
|
|
116
|
+
|| hasUnterminalDirectDispatchLedgerEntry(coordinatorMeshId, instanceId);
|
|
117
|
+
} catch { /* best-effort */ }
|
|
118
|
+
if (!hasActiveDispatch) return reject('coordinator_not_dispatch_target');
|
|
119
|
+
meshIdFromDirectDispatch = coordinatorMeshId;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const meshIdFromRuntime = readNonEmptyString(settings.meshNodeFor) || meshIdFromDirectDispatch;
|
|
123
|
+
|
|
124
|
+
// Worker-envelope proof of delegation. A worker can arrive carrying only the routing
|
|
125
|
+
// anchor (meshCoordinatorDaemonId) without meshNodeFor — e.g. when the node/mesh stamp
|
|
126
|
+
// was dropped on a direct dispatch or relaunch. Treat any envelope marker as proof and
|
|
127
|
+
// recover the mesh id by workspace when the runtime id is absent.
|
|
128
|
+
const hasWorkerEnvelope = Boolean(
|
|
129
|
+
meshIdFromRuntime
|
|
130
|
+
|| settings.launchedByCoordinator
|
|
131
|
+
|| coordinatorDaemonId
|
|
132
|
+
|| readNonEmptyString(settings.meshCoordinatorNodeId),
|
|
133
|
+
);
|
|
134
|
+
if (!hasWorkerEnvelope) return reject('no_worker_envelope');
|
|
135
|
+
|
|
136
|
+
const mesh = meshIdFromRuntime ? deps.getMeshById(meshIdFromRuntime) : deps.getMeshByWorkspace(workspace);
|
|
137
|
+
const meshId = meshIdFromRuntime || readNonEmptyString(mesh?.id);
|
|
138
|
+
if (!meshId) return reject('mesh_unresolved');
|
|
139
|
+
|
|
140
|
+
const targetNode = mesh?.nodes?.find((n: any) => n.workspace === workspace);
|
|
141
|
+
const runtimeNodeId = readNonEmptyString(settings.meshNodeId);
|
|
142
|
+
const nodeId = readNonEmptyString(targetNode?.id) || runtimeNodeId;
|
|
143
|
+
const nodeLabel = targetNode
|
|
144
|
+
? `Node '${targetNode.id}'`
|
|
145
|
+
: runtimeNodeId
|
|
146
|
+
? `Node '${runtimeNodeId}'`
|
|
147
|
+
: `Agent at ${workspace}`;
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
isDelegate: true,
|
|
151
|
+
meshId,
|
|
152
|
+
nodeId,
|
|
153
|
+
nodeLabel,
|
|
154
|
+
coordinatorDaemonId,
|
|
155
|
+
workspace,
|
|
156
|
+
sessionId,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// R4: fail-loud routing diagnostics
|
|
162
|
+
//
|
|
163
|
+
// A rejection is only worth surfacing when the session DID present a worker
|
|
164
|
+
// envelope but routing still couldn't complete (`mesh_unresolved`). Before R4
|
|
165
|
+
// that event was dropped silently — setupMeshEventForwarding returned, the
|
|
166
|
+
// completion never reached a coordinator and never landed in the pending queue,
|
|
167
|
+
// and there was no trace of why. The benign rejections (not_cli / no_workspace /
|
|
168
|
+
// no_worker_envelope / coordinator_not_dispatch_target) are ordinary non-delegate
|
|
169
|
+
// traffic and must NOT spam the ledger.
|
|
170
|
+
//
|
|
171
|
+
// The diagnostic is keyed under a single shared stream id so all unroutable drops
|
|
172
|
+
// across meshes are discoverable in one place (the dropped event has no resolvable
|
|
173
|
+
// mesh by definition). A short per-(session+event) dedup window keeps a chatty
|
|
174
|
+
// session from flooding the ledger.
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
/** Ledger stream that collects delivery_unroutable diagnostics (no real mesh id exists for them). */
|
|
178
|
+
export const UNROUTABLE_DIAGNOSTIC_STREAM = '__unroutable__';
|
|
179
|
+
|
|
180
|
+
const UNROUTABLE_DIAGNOSTIC_DEDUP_MS = 60 * 1000;
|
|
181
|
+
const recentUnroutableDiagnostics = new Map<string, number>();
|
|
182
|
+
|
|
183
|
+
export function __resetUnroutableDiagnosticsForTests(): void {
|
|
184
|
+
recentUnroutableDiagnostics.clear();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** True only for rejections that represent a silently-dropped delegate event worth a diagnostic. */
|
|
188
|
+
export function isUnroutableDelegateRejection(routing: WorkerDelegateRouting): boolean {
|
|
189
|
+
return !routing.isDelegate && routing.rejectionReason === 'mesh_unresolved';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* R4: record that a worker event with a valid envelope could not be routed to a coordinator.
|
|
194
|
+
* Writes a `delivery_unroutable` ledger entry (deduped within a short window) so operators see
|
|
195
|
+
* WHY a completion never arrived instead of it vanishing. No-op for non-diagnostic rejections.
|
|
196
|
+
*/
|
|
197
|
+
export function recordUnroutableDelegateEvent(routing: WorkerDelegateRouting, eventName: string): boolean {
|
|
198
|
+
if (!isUnroutableDelegateRejection(routing)) return false;
|
|
199
|
+
|
|
200
|
+
const dedupKey = `${routing.sessionId}::${eventName}::${routing.workspace}`;
|
|
201
|
+
const now = Date.now();
|
|
202
|
+
const last = recentUnroutableDiagnostics.get(dedupKey);
|
|
203
|
+
if (last !== undefined && now - last < UNROUTABLE_DIAGNOSTIC_DEDUP_MS) return false;
|
|
204
|
+
recentUnroutableDiagnostics.set(dedupKey, now);
|
|
205
|
+
// Opportunistic sweep so the map can't grow unbounded.
|
|
206
|
+
if (recentUnroutableDiagnostics.size > 256) {
|
|
207
|
+
for (const [key, ts] of recentUnroutableDiagnostics) {
|
|
208
|
+
if (now - ts >= UNROUTABLE_DIAGNOSTIC_DEDUP_MS) recentUnroutableDiagnostics.delete(key);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
appendLedgerEntry(UNROUTABLE_DIAGNOSTIC_STREAM, {
|
|
214
|
+
kind: 'delivery_unroutable',
|
|
215
|
+
sessionId: routing.sessionId || undefined,
|
|
216
|
+
payload: {
|
|
217
|
+
event: eventName,
|
|
218
|
+
reason: routing.rejectionReason,
|
|
219
|
+
workspace: routing.workspace || undefined,
|
|
220
|
+
coordinatorDaemonId: routing.coordinatorDaemonId || undefined,
|
|
221
|
+
detail: 'Worker envelope was present but no mesh could be resolved; the event could not be routed to a coordinator.',
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
LOG.warn('MeshEvents', `delivery_unroutable: ${eventName} from session ${routing.sessionId || '(unknown)'} at ${routing.workspace || '(no workspace)'} — envelope present but mesh unresolved`);
|
|
225
|
+
return true;
|
|
226
|
+
} catch (e: any) {
|
|
227
|
+
LOG.warn('MeshEvents', `Failed to record delivery_unroutable diagnostic: ${e?.message || e}`);
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export interface UnroutableDeliveryDiagnostic {
|
|
233
|
+
timestamp: string;
|
|
234
|
+
event: string;
|
|
235
|
+
sessionId?: string;
|
|
236
|
+
workspace?: string;
|
|
237
|
+
coordinatorDaemonId?: string;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* R4 visibility: recent delivery_unroutable diagnostics, newest first. Surfaced in mesh_status
|
|
242
|
+
* so an operator/coordinator can see that completions were dropped (envelope present, mesh
|
|
243
|
+
* unresolved) rather than the drops being invisible in a ledger nobody reads. The diagnostics
|
|
244
|
+
* live in a single shared stream because an unroutable event has no resolvable mesh.
|
|
245
|
+
*/
|
|
246
|
+
export function getRecentUnroutableDeliveries(opts?: { sinceMs?: number; limit?: number }): UnroutableDeliveryDiagnostic[] {
|
|
247
|
+
const sinceMs = opts?.sinceMs ?? 60 * 60 * 1000; // last hour by default
|
|
248
|
+
const limit = opts?.limit ?? 20;
|
|
249
|
+
let entries: ReturnType<typeof readLedgerEntries>;
|
|
250
|
+
try {
|
|
251
|
+
entries = readLedgerEntries(UNROUTABLE_DIAGNOSTIC_STREAM, { kind: ['delivery_unroutable'], tail: 200 });
|
|
252
|
+
} catch {
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
const cutoff = Date.now() - sinceMs;
|
|
256
|
+
const out: UnroutableDeliveryDiagnostic[] = [];
|
|
257
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
258
|
+
const entry = entries[i];
|
|
259
|
+
const ts = new Date(entry.timestamp).getTime();
|
|
260
|
+
if (!Number.isNaN(ts) && ts < cutoff) continue;
|
|
261
|
+
const payload = entry.payload && typeof entry.payload === 'object' ? entry.payload as Record<string, unknown> : {};
|
|
262
|
+
out.push({
|
|
263
|
+
timestamp: entry.timestamp,
|
|
264
|
+
event: readNonEmptyString(payload.event),
|
|
265
|
+
sessionId: readNonEmptyString(entry.sessionId) || readNonEmptyString(payload.sessionId) || undefined,
|
|
266
|
+
workspace: readNonEmptyString(payload.workspace) || undefined,
|
|
267
|
+
coordinatorDaemonId: readNonEmptyString(payload.coordinatorDaemonId) || undefined,
|
|
268
|
+
});
|
|
269
|
+
if (out.length >= limit) break;
|
|
270
|
+
}
|
|
271
|
+
return out;
|
|
272
|
+
}
|
|
@@ -117,6 +117,21 @@ export class MeshRuntimeStore {
|
|
|
117
117
|
expires_at INTEGER NOT NULL
|
|
118
118
|
);
|
|
119
119
|
|
|
120
|
+
-- R3: idempotent coordinator inbox. When a terminal/force-inject event is
|
|
121
|
+
-- direct-injected into a LIVE local CLI coordinator (coord.onEvent('send_message')),
|
|
122
|
+
-- we record (coordinator_daemon_id, fingerprint) here. That same coordinator also
|
|
123
|
+
-- polls get_pending_mesh_events, which would re-deliver the queued copy of the very
|
|
124
|
+
-- event it just received in its PTY → user sees the completion twice. The drain for
|
|
125
|
+
-- a coordinator daemon filters out events already direct-delivered to it, giving
|
|
126
|
+
-- exactly-once-per-coordinator while keeping the queue for other consumers (idle /
|
|
127
|
+
-- MCP-only / remote) that did NOT receive the direct inject.
|
|
128
|
+
CREATE TABLE IF NOT EXISTS mesh_direct_delivered_events (
|
|
129
|
+
coordinator_daemon_id TEXT NOT NULL,
|
|
130
|
+
fingerprint TEXT NOT NULL,
|
|
131
|
+
expires_at INTEGER NOT NULL,
|
|
132
|
+
PRIMARY KEY (coordinator_daemon_id, fingerprint)
|
|
133
|
+
);
|
|
134
|
+
|
|
120
135
|
CREATE TABLE IF NOT EXISTS mesh_direct_dispatches (
|
|
121
136
|
task_id TEXT PRIMARY KEY,
|
|
122
137
|
mesh_id TEXT NOT NULL,
|
|
@@ -281,6 +296,28 @@ export class MeshRuntimeStore {
|
|
|
281
296
|
this.db.prepare('DELETE FROM mesh_completion_fingerprints WHERE expires_at <= ?').run(Date.now());
|
|
282
297
|
}
|
|
283
298
|
|
|
299
|
+
// R3: record that an event (by pending-event fingerprint) was direct-injected into a live
|
|
300
|
+
// coordinator on the given daemon, so that coordinator's own drain skips the queued copy.
|
|
301
|
+
recordDirectDelivered(coordinatorDaemonId: string, fingerprint: string, ttlMs: number): void {
|
|
302
|
+
if (!coordinatorDaemonId || !fingerprint) return;
|
|
303
|
+
this.db.prepare(
|
|
304
|
+
'INSERT OR REPLACE INTO mesh_direct_delivered_events (coordinator_daemon_id, fingerprint, expires_at) VALUES (?, ?, ?)'
|
|
305
|
+
).run(coordinatorDaemonId, fingerprint, Date.now() + ttlMs);
|
|
306
|
+
this.maybeCheckpointWal();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
wasDirectDelivered(coordinatorDaemonId: string, fingerprint: string): boolean {
|
|
310
|
+
if (!coordinatorDaemonId || !fingerprint) return false;
|
|
311
|
+
const row = this.db.prepare(
|
|
312
|
+
'SELECT 1 FROM mesh_direct_delivered_events WHERE coordinator_daemon_id = ? AND fingerprint = ? AND expires_at > ?'
|
|
313
|
+
).get(coordinatorDaemonId, fingerprint, Date.now());
|
|
314
|
+
return row !== undefined;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
sweepExpiredDirectDelivered(): void {
|
|
318
|
+
this.db.prepare('DELETE FROM mesh_direct_delivered_events WHERE expires_at <= ?').run(Date.now());
|
|
319
|
+
}
|
|
320
|
+
|
|
284
321
|
private maybeCheckpointWal(): void {
|
|
285
322
|
if (++this.walWriteCounter < MeshRuntimeStore.WAL_CHECK_INTERVAL) return;
|
|
286
323
|
this.walWriteCounter = 0;
|