@adhdev/daemon-core 0.9.82-rc.468 → 0.9.82-rc.469
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/index.d.ts +7 -5
- package/dist/index.js +683 -495
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +674 -493
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-completion-synthesis.d.ts +4 -0
- package/dist/mesh/mesh-delivery-policy.d.ts +0 -27
- package/dist/mesh/mesh-events-pending.d.ts +40 -0
- package/dist/mesh/mesh-events.d.ts +2 -2
- package/dist/mesh/mesh-ledger.d.ts +1 -1
- package/dist/mesh/mesh-reconcile-config.d.ts +6 -0
- package/dist/mesh/mesh-remote-event-pull.d.ts +15 -0
- package/dist/mesh/mesh-runtime-store.d.ts +0 -22
- package/dist/mesh/mesh-work-queue.d.ts +45 -0
- package/package.json +3 -3
- package/src/index.ts +11 -5
- package/src/mesh/coordinator-prompt.ts +15 -0
- package/src/mesh/mesh-completion-synthesis.ts +398 -0
- package/src/mesh/mesh-delivery-policy.ts +7 -38
- package/src/mesh/mesh-event-forwarding.ts +9 -10
- package/src/mesh/mesh-events-pending.ts +176 -0
- package/src/mesh/mesh-events.ts +6 -1
- package/src/mesh/mesh-ledger.ts +5 -0
- package/src/mesh/mesh-queue-assignment.ts +16 -2
- package/src/mesh/mesh-reconcile-config.ts +66 -0
- package/src/mesh/mesh-reconcile-loop.ts +21 -674
- package/src/mesh/mesh-remote-event-pull.ts +279 -0
- package/src/mesh/mesh-runtime-store.ts +46 -82
- package/src/mesh/mesh-work-queue.ts +90 -0
|
@@ -189,6 +189,10 @@ function ledgerRecordQuarantinedEvent(event: PendingMeshCoordinatorEvent, reason
|
|
|
189
189
|
...(readNonEmptyString(event.eventId) ? { eventId: event.eventId } : {}),
|
|
190
190
|
queuedAt: event.queuedAt,
|
|
191
191
|
...(finalSummary ? { finalSummary } : {}),
|
|
192
|
+
// Full original event so mesh_requeue_held_events can restore it
|
|
193
|
+
// losslessly (event_held→pending). The summary/label fields above stay
|
|
194
|
+
// for human-readable audit; `heldEvent` is the machine recovery copy.
|
|
195
|
+
heldEvent: event,
|
|
192
196
|
},
|
|
193
197
|
});
|
|
194
198
|
} catch (e: any) {
|
|
@@ -746,8 +750,11 @@ function trimPendingEventsIfNeeded(path: string): void {
|
|
|
746
750
|
nodeLabel: event.nodeLabel,
|
|
747
751
|
...(event.workspace ? { workspace: event.workspace } : {}),
|
|
748
752
|
targetCoordinatorDaemonId: event.targetCoordinatorDaemonId ?? null,
|
|
753
|
+
...(readNonEmptyString(event.eventId) ? { eventId: event.eventId } : {}),
|
|
749
754
|
queuedAt: event.queuedAt,
|
|
750
755
|
...(finalSummary ? { finalSummary } : {}),
|
|
756
|
+
// Full original event for lossless mesh_requeue_held_events restore.
|
|
757
|
+
heldEvent: event,
|
|
751
758
|
},
|
|
752
759
|
});
|
|
753
760
|
LOG.warn('MeshEvents', `Pending-events trim dropping undelivered ${event.event} for mesh ${event.meshId} — recorded to ledger (recoverable)`);
|
|
@@ -1303,6 +1310,175 @@ export function __persistUnstampedPendingEventForTests(event: PendingMeshCoordin
|
|
|
1303
1310
|
return persistPendingMeshCoordinatorEvent(event);
|
|
1304
1311
|
}
|
|
1305
1312
|
|
|
1313
|
+
// ---------------------------------------------------------------------------
|
|
1314
|
+
// event_held → pending requeue (T6 recovery path)
|
|
1315
|
+
// ---------------------------------------------------------------------------
|
|
1316
|
+
// T6 quarantine (v2 enforce) and the pending-events trim both mirror a
|
|
1317
|
+
// destructively-drained-but-undelivered event into the ledger as a recoverable
|
|
1318
|
+
// `event_held` entry. Until now that recovery channel was audit-only: the comment
|
|
1319
|
+
// said "an operator can requeue it" but no code path did. This restores those held
|
|
1320
|
+
// events to the pending queue so a coordinator drains them on its next poll.
|
|
1321
|
+
//
|
|
1322
|
+
// No-loss + no-double-requeue invariants:
|
|
1323
|
+
// • The full original event rides on the held payload as `heldEvent`, so the
|
|
1324
|
+
// restore is byte-for-byte (metadataEvent, coordinatorMessage, v2 envelope).
|
|
1325
|
+
// • queuePendingMeshCoordinatorEvent runs the normal dedup (fingerprint / eventId),
|
|
1326
|
+
// so an event still live in the queue is not duplicated.
|
|
1327
|
+
// • Every held entry that has ALREADY been requeued is marked with an
|
|
1328
|
+
// `event_held_requeued` ledger entry keyed by the source held-entry id; a later
|
|
1329
|
+
// pass skips those ids, so calling the tool twice does not requeue the same held
|
|
1330
|
+
// event twice.
|
|
1331
|
+
|
|
1332
|
+
/** Narrowing filter for {@link requeueHeldMeshCoordinatorEvents}, scoped within one mesh. */
|
|
1333
|
+
export interface MeshHeldEventRequeueFilter {
|
|
1334
|
+
/** Restore only held events whose worker task id matches (from the held event's metadata/taskId). */
|
|
1335
|
+
taskId?: string;
|
|
1336
|
+
/** Restore only held events originating from this node. */
|
|
1337
|
+
nodeId?: string;
|
|
1338
|
+
/** Restore only held events of this event name (e.g. 'session:completed'). */
|
|
1339
|
+
event?: string;
|
|
1340
|
+
/** Restore only held entries recorded at/after this ISO timestamp. */
|
|
1341
|
+
since?: string;
|
|
1342
|
+
/** Restore only held entries with this hold reason (e.g. 'pending_trim_dropped'). */
|
|
1343
|
+
reason?: string;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
export interface MeshHeldEventRequeueResult {
|
|
1347
|
+
meshId: string;
|
|
1348
|
+
/** event_held entries considered after the filter. */
|
|
1349
|
+
matched: number;
|
|
1350
|
+
/** entries skipped because a prior requeue already recovered them. */
|
|
1351
|
+
alreadyRequeued: number;
|
|
1352
|
+
/** entries skipped because they carried no restorable original event / were not recoverable. */
|
|
1353
|
+
unrecoverable: number;
|
|
1354
|
+
/** entries handed to the pending queue (some may have been dedup-suppressed downstream). */
|
|
1355
|
+
requeued: number;
|
|
1356
|
+
/** of `requeued`, how many the pending-queue dedup collapsed onto a live event. */
|
|
1357
|
+
dedupSuppressed: number;
|
|
1358
|
+
entries: Array<{
|
|
1359
|
+
heldEntryId: string;
|
|
1360
|
+
event: string;
|
|
1361
|
+
nodeId?: string;
|
|
1362
|
+
taskId?: string;
|
|
1363
|
+
reason?: string;
|
|
1364
|
+
outcome: 'requeued' | 'already_requeued' | 'unrecoverable';
|
|
1365
|
+
}>;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
/** Read the taskId a held event carried, checking the restored event then the audit payload. */
|
|
1369
|
+
function readHeldTaskId(restored: PendingMeshCoordinatorEvent | undefined, payload: Record<string, unknown>): string {
|
|
1370
|
+
const fromMeta = restored?.metadataEvent && typeof restored.metadataEvent === 'object'
|
|
1371
|
+
? readNonEmptyString((restored.metadataEvent as Record<string, unknown>).taskId)
|
|
1372
|
+
: '';
|
|
1373
|
+
return fromMeta || readNonEmptyString(payload.taskId) || '';
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
/**
|
|
1377
|
+
* Restore recoverable `event_held` ledger entries back to the pending coordinator
|
|
1378
|
+
* queue for `meshId`. See the block comment above for the no-loss / no-double-requeue
|
|
1379
|
+
* invariants. Returns per-entry outcomes for the caller to surface.
|
|
1380
|
+
*/
|
|
1381
|
+
export function requeueHeldMeshCoordinatorEvents(
|
|
1382
|
+
meshId: string,
|
|
1383
|
+
filter?: MeshHeldEventRequeueFilter,
|
|
1384
|
+
): MeshHeldEventRequeueResult {
|
|
1385
|
+
const result: MeshHeldEventRequeueResult = {
|
|
1386
|
+
meshId,
|
|
1387
|
+
matched: 0,
|
|
1388
|
+
alreadyRequeued: 0,
|
|
1389
|
+
unrecoverable: 0,
|
|
1390
|
+
requeued: 0,
|
|
1391
|
+
dedupSuppressed: 0,
|
|
1392
|
+
entries: [],
|
|
1393
|
+
};
|
|
1394
|
+
|
|
1395
|
+
const all = readLedgerEntries(meshId);
|
|
1396
|
+
// held-entry ids already recovered by a prior requeue pass (dedup key = source id).
|
|
1397
|
+
const requeuedIds = new Set<string>();
|
|
1398
|
+
for (const entry of all) {
|
|
1399
|
+
if (entry.kind !== 'event_held_requeued') continue;
|
|
1400
|
+
const id = readNonEmptyString(entry.payload?.heldEntryId);
|
|
1401
|
+
if (id) requeuedIds.add(id);
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
const sinceMs = filter?.since ? new Date(filter.since).getTime() : NaN;
|
|
1405
|
+
const wantEvent = readNonEmptyString(filter?.event);
|
|
1406
|
+
const wantNode = readNonEmptyString(filter?.nodeId);
|
|
1407
|
+
const wantTask = readNonEmptyString(filter?.taskId);
|
|
1408
|
+
const wantReason = readNonEmptyString(filter?.reason);
|
|
1409
|
+
|
|
1410
|
+
for (const entry of all) {
|
|
1411
|
+
if (entry.kind !== 'event_held') continue;
|
|
1412
|
+
const payload = (entry.payload && typeof entry.payload === 'object') ? entry.payload : {};
|
|
1413
|
+
if (payload.recoverable !== true) continue;
|
|
1414
|
+
|
|
1415
|
+
// Reconstruct the original event: prefer the full `heldEvent` copy; fall back to
|
|
1416
|
+
// the flat audit fields for entries written before the copy was embedded.
|
|
1417
|
+
const restored: PendingMeshCoordinatorEvent | undefined =
|
|
1418
|
+
(payload.heldEvent && typeof payload.heldEvent === 'object')
|
|
1419
|
+
? { ...(payload.heldEvent as PendingMeshCoordinatorEvent) }
|
|
1420
|
+
: undefined;
|
|
1421
|
+
|
|
1422
|
+
const eventName = restored?.event || readNonEmptyString(payload.event);
|
|
1423
|
+
const nodeId = restored?.nodeId || entry.nodeId || readNonEmptyString((payload as any).nodeId) || undefined;
|
|
1424
|
+
const taskId = readHeldTaskId(restored, payload as Record<string, unknown>);
|
|
1425
|
+
const reason = readNonEmptyString(payload.reason) || undefined;
|
|
1426
|
+
|
|
1427
|
+
// Apply the caller filter within the mesh scope.
|
|
1428
|
+
if (wantEvent && eventName !== wantEvent) continue;
|
|
1429
|
+
if (wantNode && nodeId !== wantNode) continue;
|
|
1430
|
+
if (wantTask && taskId !== wantTask) continue;
|
|
1431
|
+
if (wantReason && reason !== wantReason) continue;
|
|
1432
|
+
if (filter?.since && !Number.isNaN(sinceMs) && new Date(entry.timestamp).getTime() < sinceMs) continue;
|
|
1433
|
+
|
|
1434
|
+
result.matched++;
|
|
1435
|
+
|
|
1436
|
+
if (requeuedIds.has(entry.id)) {
|
|
1437
|
+
result.alreadyRequeued++;
|
|
1438
|
+
result.entries.push({ heldEntryId: entry.id, event: eventName, ...(nodeId ? { nodeId } : {}), ...(taskId ? { taskId } : {}), ...(reason ? { reason } : {}), outcome: 'already_requeued' });
|
|
1439
|
+
continue;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
if (!restored || !readNonEmptyString(restored.event) || !readNonEmptyString(restored.meshId)) {
|
|
1443
|
+
result.unrecoverable++;
|
|
1444
|
+
result.entries.push({ heldEntryId: entry.id, event: eventName, ...(nodeId ? { nodeId } : {}), ...(taskId ? { taskId } : {}), ...(reason ? { reason } : {}), outcome: 'unrecoverable' });
|
|
1445
|
+
continue;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
// Restore to pending. queuePendingMeshCoordinatorEvent re-stamps/dedups; a live
|
|
1449
|
+
// duplicate is suppressed there (returns true) so we never double-deliver.
|
|
1450
|
+
const beforeDup = hasPendingCoordinatorEventDuplicate(restored);
|
|
1451
|
+
let ok = false;
|
|
1452
|
+
try {
|
|
1453
|
+
ok = queuePendingMeshCoordinatorEvent(restored);
|
|
1454
|
+
} catch (e: any) {
|
|
1455
|
+
LOG.warn('MeshEvents', `Requeue of held ${eventName} for mesh ${meshId} failed: ${e?.message || e}`);
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// Mark the source held entry so a second pass skips it, regardless of whether the
|
|
1459
|
+
// queue dedup collapsed it (the recovery attempt is what we dedup on, not delivery).
|
|
1460
|
+
appendLedgerEntry(meshId, {
|
|
1461
|
+
kind: 'event_held_requeued',
|
|
1462
|
+
...(nodeId ? { nodeId } : {}),
|
|
1463
|
+
payload: {
|
|
1464
|
+
heldEntryId: entry.id,
|
|
1465
|
+
event: eventName,
|
|
1466
|
+
requeued: ok,
|
|
1467
|
+
...(taskId ? { taskId } : {}),
|
|
1468
|
+
...(reason ? { reason } : {}),
|
|
1469
|
+
...(beforeDup ? { dedupSuppressed: true } : {}),
|
|
1470
|
+
},
|
|
1471
|
+
});
|
|
1472
|
+
requeuedIds.add(entry.id);
|
|
1473
|
+
|
|
1474
|
+
result.requeued++;
|
|
1475
|
+
if (beforeDup) result.dedupSuppressed++;
|
|
1476
|
+
result.entries.push({ heldEntryId: entry.id, event: eventName, ...(nodeId ? { nodeId } : {}), ...(taskId ? { taskId } : {}), ...(reason ? { reason } : {}), outcome: 'requeued' });
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
return result;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1306
1482
|
/** Explicitly clear all pending coordinator events for a mesh (and coordinator if scoped). */
|
|
1307
1483
|
export function clearPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string): void {
|
|
1308
1484
|
if (!meshId) return;
|
package/src/mesh/mesh-events.ts
CHANGED
|
@@ -5,12 +5,17 @@
|
|
|
5
5
|
// New code should import directly from the relevant sub-module.
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
7
7
|
|
|
8
|
-
export type {
|
|
8
|
+
export type {
|
|
9
|
+
PendingMeshCoordinatorEvent,
|
|
10
|
+
MeshHeldEventRequeueFilter,
|
|
11
|
+
MeshHeldEventRequeueResult,
|
|
12
|
+
} from './mesh-events-pending.js';
|
|
9
13
|
export {
|
|
10
14
|
queuePendingMeshCoordinatorEvent,
|
|
11
15
|
drainPendingMeshCoordinatorEvents,
|
|
12
16
|
getPendingMeshCoordinatorEvents,
|
|
13
17
|
clearPendingMeshCoordinatorEvents,
|
|
18
|
+
requeueHeldMeshCoordinatorEvents,
|
|
14
19
|
serializeV2EnvelopeToWire,
|
|
15
20
|
readV2EnvelopeFromWire,
|
|
16
21
|
getMeshV2DrainCounters,
|
package/src/mesh/mesh-ledger.ts
CHANGED
|
@@ -49,6 +49,11 @@ export type MeshLedgerKind =
|
|
|
49
49
|
| 'delivery_unroutable'
|
|
50
50
|
| 'direct_dispatch_pruned'
|
|
51
51
|
| 'event_held'
|
|
52
|
+
// Audit marker written by mesh_requeue_held_events when a recoverable `event_held`
|
|
53
|
+
// entry is restored to the pending queue (event_held→pending). Keyed by the source
|
|
54
|
+
// held-ledger-entry id so a second requeue pass skips already-recovered entries
|
|
55
|
+
// (no double-requeue). payload: { heldEntryId, event, requeued: boolean, reason?, dedupSuppressed?: boolean }
|
|
56
|
+
| 'event_held_requeued'
|
|
52
57
|
| 'task_reclaimed'
|
|
53
58
|
// Gap2-A: a coordinator-recorded operating note — a runtime-accumulated
|
|
54
59
|
// lesson (provider quirk, pattern to avoid, recovery lesson) persisted in
|
|
@@ -6,7 +6,7 @@ import { getMesh } from '../config/mesh-config.js';
|
|
|
6
6
|
import { detectCLI } from '../detection/cli-detector.js';
|
|
7
7
|
import { LOG } from '../logging/logger.js';
|
|
8
8
|
import { appendLedgerEntry } from './mesh-ledger.js';
|
|
9
|
-
import { buildMeshNodeCapabilityTags, nodeSatisfiesRequiredTags, claimNextTask, updateTaskStatus, getQueue, recordTaskAutoLaunch, getActiveDirectDispatches, isTaskReadonly, taskDependenciesSatisfied } from './mesh-work-queue.js';
|
|
9
|
+
import { buildMeshNodeCapabilityTags, nodeSatisfiesRequiredTags, claimNextTask, updateTaskStatus, getQueue, recordTaskAutoLaunch, getActiveDirectDispatches, isTaskReadonly, taskDependenciesSatisfied, meshTaskNotBeforeReady, meshTaskPriorityRank } from './mesh-work-queue.js';
|
|
10
10
|
import type { MeshWorkQueueEntry } from './mesh-work-queue.js';
|
|
11
11
|
import { fastForwardMeshNode } from './mesh-fast-forward.js';
|
|
12
12
|
import { createSessionDelivery, updateSessionDeliveryStatus } from './mesh-delivery-policy.js';
|
|
@@ -1563,7 +1563,13 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
|
|
|
1563
1563
|
// so the dependency gate below sees the terminal state of every referenced
|
|
1564
1564
|
// dependency, not just the still-active rows.
|
|
1565
1565
|
const statusById = new Map(queue.map(task => [task.id, task.status] as const));
|
|
1566
|
-
|
|
1566
|
+
// G6: scan higher task-level priority first so a high-priority task auto-launches its
|
|
1567
|
+
// session ahead of an older normal/low task (getQueue is FIFO; a stable sort by priority
|
|
1568
|
+
// rank descending keeps created_at order within a priority band). The claim path applies
|
|
1569
|
+
// the same ordering, so the launched session pulls the same task the scan chose.
|
|
1570
|
+
const pending = queue
|
|
1571
|
+
.filter(task => task.status === 'pending')
|
|
1572
|
+
.sort((a, b) => meshTaskPriorityRank(b.priority) - meshTaskPriorityRank(a.priority));
|
|
1567
1573
|
// AUTOLAUNCH-CLAIM-CHURN: prune await-claim backoff state for tasks of this mesh that are no
|
|
1568
1574
|
// longer pending (claimed/completed/cancelled) so the map cannot grow without bound.
|
|
1569
1575
|
{
|
|
@@ -1595,6 +1601,14 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
|
|
|
1595
1601
|
markAutoLaunch(meshId, task.id, { status: 'skipped', reason: 'dependencies_unsatisfied' });
|
|
1596
1602
|
continue;
|
|
1597
1603
|
}
|
|
1604
|
+
// G7: never spawn a session for a task still held by its notBefore gate — the launched
|
|
1605
|
+
// session would idle→claim and be refused by the SAME gate in claimNextQueueTask,
|
|
1606
|
+
// producing orphan-session churn. Skip it so a later tick (after not_before passes)
|
|
1607
|
+
// launches it. Tasks with no notBefore pass through unchanged.
|
|
1608
|
+
if (!meshTaskNotBeforeReady(task)) {
|
|
1609
|
+
markAutoLaunch(meshId, task.id, { status: 'skipped', reason: 'not_before_delayed' });
|
|
1610
|
+
continue;
|
|
1611
|
+
}
|
|
1598
1612
|
const isReadonly = isTaskReadonly(task);
|
|
1599
1613
|
if (isReadonly) {
|
|
1600
1614
|
if (activeReadonlyAssignedCount(meshId) >= maxReadonlyParallelTasks) {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// mesh-reconcile-config — reconcile-loop timing tunables + env resolvers
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Extracted from mesh-reconcile-loop.ts (A-3 god-module decomposition, pure move,
|
|
5
|
+
// no behavior change). Holds the loop-cadence tunables and their env-override
|
|
6
|
+
// resolvers. Each resolver reads MESH_*_MS from the environment and clamps the
|
|
7
|
+
// value so a mis-set env cannot make the loop pathological. Single-consumer
|
|
8
|
+
// deadline constants that live next to their sole reader (e.g.
|
|
9
|
+
// ASSIGNED_STRANDED_DEADLINE_MS, STRICT_SESSION_MATCH_TTL_MS) intentionally stay
|
|
10
|
+
// in mesh-reconcile-loop.ts — only the shared loop-cadence tunables move here.
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
import { readNonEmptyString } from './mesh-events-utils.js';
|
|
14
|
+
import { resolveTunedReconcileMs } from './mesh-reconcile-acked-hold.js';
|
|
15
|
+
|
|
16
|
+
// Default reconcile cadence. approval/completion notifications to a live CLI
|
|
17
|
+
// coordinator land within at most one interval. Overridable via env for tuning.
|
|
18
|
+
export const DEFAULT_RECONCILE_INTERVAL_MS = 4_000;
|
|
19
|
+
|
|
20
|
+
// PHASE 5 (auto-prune) conservative age gate. A direct dispatch whose node/session is
|
|
21
|
+
// orphaned (no longer in the live mesh) is only auto-pruned once it is at least this old,
|
|
22
|
+
// measured from its dispatch time. This protects against a node/session that is only
|
|
23
|
+
// *transiently* invisible (a momentary probe failure, a daemon restart) being pruned the
|
|
24
|
+
// instant it disappears. The MANUAL prune (mesh_prune_stale_direct) has no age gate — an
|
|
25
|
+
// operator pruning explicitly wants the orphan gone now. Overridable via env for tuning.
|
|
26
|
+
export const DEFAULT_AUTO_PRUNE_MIN_AGE_MS = 24 * 60 * 60_000; // 24h
|
|
27
|
+
|
|
28
|
+
export function resolveAutoPruneMinAgeMs(): number {
|
|
29
|
+
const raw = readNonEmptyString(process.env.MESH_AUTO_PRUNE_MIN_AGE_MS);
|
|
30
|
+
if (raw) {
|
|
31
|
+
const parsed = Number.parseInt(raw, 10);
|
|
32
|
+
// Clamp to [1h, 30d] so a mis-set env can't make the gate pathologically aggressive
|
|
33
|
+
// (prune the moment something blinks) or effectively disable it forever.
|
|
34
|
+
if (Number.isFinite(parsed) && parsed >= 60 * 60_000 && parsed <= 30 * 24 * 60 * 60_000) return parsed;
|
|
35
|
+
}
|
|
36
|
+
return DEFAULT_AUTO_PRUNE_MIN_AGE_MS;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// PTY-OVERTRUST-DRAIN (Defect B, fix B). Age-based escape for the
|
|
40
|
+
// `generating_no_idle_coordinator` hold. Fix A makes the drain predicate read the RAW
|
|
41
|
+
// adapter (mask-stripped), so the common mask-driven false-busy is gone. But a hold can
|
|
42
|
+
// still arise from a genuine status-source desync that fix A does not reach (e.g. the
|
|
43
|
+
// adapter raw itself momentarily reads generating while the coordinator is actually at a
|
|
44
|
+
// turn end). This is a TIME-BASED BACKSTOP: when a mesh's pending terminal events have
|
|
45
|
+
// been held this long, re-confirm the coordinator's RAW adapter idle on the tick and, if
|
|
46
|
+
// it is genuinely idle, drain ONCE. It NEVER injects into a genuinely-generating PTY —
|
|
47
|
+
// the re-confirmation gates on raw adapter idle, so the intentional removal of
|
|
48
|
+
// force-inject-into-generating (data-loss) is preserved. Default 12s = 3 reconcile ticks
|
|
49
|
+
// at the 4s cadence: long enough that a normal mid-turn settle is not pre-empted, short
|
|
50
|
+
// enough that a desync-stranded completion is not held for minutes. Env-tunable.
|
|
51
|
+
export const DEFAULT_PENDING_HELD_DRAIN_ESCALATE_MS = 12_000;
|
|
52
|
+
|
|
53
|
+
export function resolvePendingHeldDrainEscalateMs(): number {
|
|
54
|
+
// Floor 4s (one tick) so a mis-set env cannot make the escape race a normal settle;
|
|
55
|
+
// ceiling 5min so it cannot be disabled into a permanent strand.
|
|
56
|
+
return resolveTunedReconcileMs('MESH_PENDING_HELD_DRAIN_ESCALATE_MS', DEFAULT_PENDING_HELD_DRAIN_ESCALATE_MS, 4_000, 5 * 60_000);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function resolveReconcileIntervalMs(): number {
|
|
60
|
+
const raw = readNonEmptyString(process.env.MESH_RECONCILE_INTERVAL_MS);
|
|
61
|
+
if (raw) {
|
|
62
|
+
const parsed = Number.parseInt(raw, 10);
|
|
63
|
+
if (Number.isFinite(parsed) && parsed >= 1_000 && parsed <= 60_000) return parsed;
|
|
64
|
+
}
|
|
65
|
+
return DEFAULT_RECONCILE_INTERVAL_MS;
|
|
66
|
+
}
|