@tokenfactory/acc-runner 0.40.27 → 0.40.29
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/attachments/allowlist.d.ts +16 -0
- package/dist/attachments/allowlist.d.ts.map +1 -0
- package/dist/attachments/allowlist.js +34 -0
- package/dist/attachments/allowlist.js.map +1 -0
- package/dist/attachments/content-block.d.ts +16 -0
- package/dist/attachments/content-block.d.ts.map +1 -0
- package/dist/attachments/content-block.js +38 -0
- package/dist/attachments/content-block.js.map +1 -0
- package/dist/attachments/fetch-attachment.d.ts +24 -0
- package/dist/attachments/fetch-attachment.d.ts.map +1 -0
- package/dist/attachments/fetch-attachment.js +55 -0
- package/dist/attachments/fetch-attachment.js.map +1 -0
- package/dist/attachments/index.d.ts +21 -0
- package/dist/attachments/index.d.ts.map +1 -0
- package/dist/attachments/index.js +20 -0
- package/dist/attachments/index.js.map +1 -0
- package/dist/attachments/types.d.ts +39 -0
- package/dist/attachments/types.d.ts.map +1 -0
- package/dist/attachments/types.js +9 -0
- package/dist/attachments/types.js.map +1 -0
- package/dist/chat-cancel/cancel-controller.d.ts +77 -0
- package/dist/chat-cancel/cancel-controller.d.ts.map +1 -0
- package/dist/chat-cancel/cancel-controller.js +148 -0
- package/dist/chat-cancel/cancel-controller.js.map +1 -0
- package/dist/chat-cancel/cancel-source.d.ts +37 -0
- package/dist/chat-cancel/cancel-source.d.ts.map +1 -0
- package/dist/chat-cancel/cancel-source.js +26 -0
- package/dist/chat-cancel/cancel-source.js.map +1 -0
- package/dist/chat-cancel/child-terminator.d.ts +37 -0
- package/dist/chat-cancel/child-terminator.d.ts.map +1 -0
- package/dist/chat-cancel/child-terminator.js +81 -0
- package/dist/chat-cancel/child-terminator.js.map +1 -0
- package/dist/chat-cancel/index.d.ts +21 -0
- package/dist/chat-cancel/index.d.ts.map +1 -0
- package/dist/chat-cancel/index.js +20 -0
- package/dist/chat-cancel/index.js.map +1 -0
- package/dist/chat-cancel/types.d.ts +69 -0
- package/dist/chat-cancel/types.d.ts.map +1 -0
- package/dist/chat-cancel/types.js +7 -0
- package/dist/chat-cancel/types.js.map +1 -0
- package/dist/claim-deadman/deadman.d.ts +152 -0
- package/dist/claim-deadman/deadman.d.ts.map +1 -0
- package/dist/claim-deadman/deadman.js +176 -0
- package/dist/claim-deadman/deadman.js.map +1 -0
- package/dist/claim-deadman/heartbeat-signal.d.ts +48 -0
- package/dist/claim-deadman/heartbeat-signal.d.ts.map +1 -0
- package/dist/claim-deadman/heartbeat-signal.js +31 -0
- package/dist/claim-deadman/heartbeat-signal.js.map +1 -0
- package/dist/claim-deadman/index.d.ts +19 -0
- package/dist/claim-deadman/index.d.ts.map +1 -0
- package/dist/claim-deadman/index.js +17 -0
- package/dist/claim-deadman/index.js.map +1 -0
- package/dist/clarification.d.ts +48 -0
- package/dist/clarification.d.ts.map +1 -0
- package/dist/clarification.js +114 -0
- package/dist/clarification.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +20 -0
- package/dist/cli.js.map +1 -1
- package/dist/watch.d.ts +74 -0
- package/dist/watch.d.ts.map +1 -1
- package/dist/watch.js +289 -1
- package/dist/watch.js.map +1 -1
- package/package.json +1 -1
package/dist/watch.js
CHANGED
|
@@ -55,6 +55,7 @@ import { getChatEngine } from "./engines/registry.js";
|
|
|
55
55
|
// a `watch` runner — the runner serves its bound user's chat turns behind their
|
|
56
56
|
// companion. Glue lives in watch-chat/wire.ts to keep this file the task loop.
|
|
57
57
|
import { startFleetChatServing, } from "./watch-chat/wire.js";
|
|
58
|
+
import { ClaimDeadman, readDispatchSignal, DEFAULT_DEADMAN_CHECK_MS, DEFAULT_FALLBACK_POLL_MS, CLAIM_CHANNEL_WEDGED_VERB, CLAIM_CHANNEL_RECOVERED_VERB, CLAIM_CHANNEL_REBUILD_VERB, CLAIM_CHANNEL_FALLBACK_VERB, } from "./claim-deadman/index.js";
|
|
58
59
|
import { unknownAccountIdentity } from "./engines/account-identity.js";
|
|
59
60
|
import { checkVersion, compareSemver } from "./version-check.js";
|
|
60
61
|
import { PACKAGE_VERSION } from "./pkg-version.js";
|
|
@@ -120,6 +121,22 @@ const DEFAULT_CLAIM_WATCHDOG_CHECK_MS = 30_000;
|
|
|
120
121
|
// regressions of the same shape. Default 60s — prompt recovery without a third
|
|
121
122
|
// frequent DB poll racing the watchdog. Knob: `claim_backstop_interval_ms`.
|
|
122
123
|
const DEFAULT_CLAIM_BACKSTOP_INTERVAL_MS = 60_000;
|
|
124
|
+
// CLAIM-DEADMAN: dead-man switch for a WEDGED claim channel
|
|
125
|
+
// that the 90s watchdog resubscribe can't clear. Live incident 2026-07-22: a
|
|
126
|
+
// runner heartbeated fine (hb_age 0min) but its Realtime claim subscription was
|
|
127
|
+
// dead AND the watchdog resubscribe had itself stopped firing — 12 queued / 0
|
|
128
|
+
// running with two online runners, cleared only by a manual restart. The deadman
|
|
129
|
+
// tracks pending-dispatch (work the server is offering) vs the last processed
|
|
130
|
+
// claim; when work is pending and no claim lands for CLAIM_DEADMAN_WEDGE_MS
|
|
131
|
+
// (default 120s, > the 90s watchdog so the cheap resubscribe gets first crack)
|
|
132
|
+
// it (1) TEARS DOWN + FULLY REBUILDS the Realtime client up to
|
|
133
|
+
// CLAIM_DEADMAN_MAX_REBUILDS times, then (2) falls back to degraded DB-poll
|
|
134
|
+
// claiming every CLAIM_DEADMAN_FALLBACK_POLL_MS. CLAIM_DEADMAN_CHECK_MS is the
|
|
135
|
+
// evaluation cadence. Tests override all of these for speed. See
|
|
136
|
+
// packages/acc-runner/src/claim-deadman/deadman.ts + docs/acc/CLAIM_DEADMAN.md.
|
|
137
|
+
const DEFAULT_CLAIM_DEADMAN_WEDGE_MS = 120_000;
|
|
138
|
+
const DEFAULT_CLAIM_DEADMAN_REBUILD_GRACE_MS = 15_000;
|
|
139
|
+
const DEFAULT_CLAIM_DEADMAN_MAX_REBUILDS = 2;
|
|
123
140
|
// GIT-AUTH-ALERT: how often the runner probes its base clone's git credential
|
|
124
141
|
// (a masked `ls-remote origin` dry-run). The same 60s cadence detects the first
|
|
125
142
|
// failure AND polls for recovery — while degraded, task-lane claiming is paused
|
|
@@ -208,6 +225,13 @@ function enqueue(state, taskId, factory) {
|
|
|
208
225
|
state.seen.add(taskId);
|
|
209
226
|
state.queue.push(taskId);
|
|
210
227
|
touchActivity(state);
|
|
228
|
+
// CLAIM-DEADMAN: a claim was processed via SOME path
|
|
229
|
+
// (realtime / poll / scan / watchdog / the deadman's own DB-poll fallback).
|
|
230
|
+
// Reset the deadman's staleness clock so a runner that IS claiming — even
|
|
231
|
+
// slowly, even degraded — is never treated as wedged. Recovery itself is
|
|
232
|
+
// declared by the offered backlog draining (observeDispatchSignal), so this
|
|
233
|
+
// never prematurely clears a wedge episode.
|
|
234
|
+
state.claimDeadman?.observeClaimProcessed();
|
|
211
235
|
void pump(state, factory);
|
|
212
236
|
return true;
|
|
213
237
|
}
|
|
@@ -656,6 +680,230 @@ async function claimBackstopScan(state, factory) {
|
|
|
656
680
|
state.backstopScanning = false;
|
|
657
681
|
}
|
|
658
682
|
}
|
|
683
|
+
/**
|
|
684
|
+
* CLAIM-DEADMAN: a runner may have free capacity but be
|
|
685
|
+
* deliberately not claiming (quarantined, capacity-paused, review-claim-paused,
|
|
686
|
+
* or already at its effective task-claim ceiling). In those states pending work
|
|
687
|
+
* SHOULD sit queued, so it must NOT be read as a wedge. Mirrors the
|
|
688
|
+
* claimBackstopScan guard. Returns true only when the runner both wants to and
|
|
689
|
+
* can claim another task.
|
|
690
|
+
*/
|
|
691
|
+
function deadmanCanClaim(state) {
|
|
692
|
+
if (state.quarantined || state.pausedCapacity || state.capacityClaimPaused) {
|
|
693
|
+
return false;
|
|
694
|
+
}
|
|
695
|
+
const limit = computeTaskClaimLimit({
|
|
696
|
+
concurrencyLimit: state.concurrencyLimit,
|
|
697
|
+
reservedReviewSlots: state.reservedReviewSlots,
|
|
698
|
+
capacityProbe: state.capacityProbe,
|
|
699
|
+
claimPaused: state.capacityClaimPaused,
|
|
700
|
+
});
|
|
701
|
+
return state.running.size < limit;
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* CLAIM-DEADMAN: parse the pending-dispatch fields off a `heartbeat_runner`
|
|
705
|
+
* response. Tolerant of a scalar row, a single-row array, or null (today's
|
|
706
|
+
* void return) — returns null unless a numeric `pending_dispatch` is present, so
|
|
707
|
+
* a pre-extension server leaves the deadman on its assigned-queued read.
|
|
708
|
+
*/
|
|
709
|
+
export function parseHeartbeatDeadmanFields(data) {
|
|
710
|
+
const row = Array.isArray(data) ? data[0] : data;
|
|
711
|
+
if (!row || typeof row !== "object")
|
|
712
|
+
return null;
|
|
713
|
+
const r = row;
|
|
714
|
+
if (typeof r.pending_dispatch !== "number")
|
|
715
|
+
return null;
|
|
716
|
+
return {
|
|
717
|
+
pending_dispatch: Math.max(0, Math.floor(r.pending_dispatch)),
|
|
718
|
+
suspect: Boolean(r.suspect),
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* CLAIM-DEADMAN: emit the audit event + operator-facing alert for a claim-
|
|
723
|
+
* channel state change. `runner.claim_channel_wedged` / `..._recovered` are the
|
|
724
|
+
* UI alert breadcrumbs; the payload NAMES the runner (id + label) so the derived
|
|
725
|
+
* notifications read model can surface "runner X's claim channel is wedged"
|
|
726
|
+
* without a join. Best-effort by contract — a logging failure never disturbs the
|
|
727
|
+
* watch loop. Also mirrors to stderr so a locally-tailed runner shows it.
|
|
728
|
+
*/
|
|
729
|
+
function emitDeadmanEvent(state, verb, payload, humanLine) {
|
|
730
|
+
process.stderr.write(`[acc-runner] ${humanLine}\n`);
|
|
731
|
+
void (async () => {
|
|
732
|
+
try {
|
|
733
|
+
await state.supabase.rpc("log_activity", {
|
|
734
|
+
p_verb: verb,
|
|
735
|
+
p_target_id: state.session.runner_id,
|
|
736
|
+
p_payload: {
|
|
737
|
+
// NAMES the runner so the derived notifications read model can surface
|
|
738
|
+
// "runner <id>'s claim channel is wedged" without a join (the UI
|
|
739
|
+
// resolves the display name from acc.runners by this id).
|
|
740
|
+
runner_id: state.session.runner_id,
|
|
741
|
+
version: PACKAGE_VERSION,
|
|
742
|
+
...payload,
|
|
743
|
+
},
|
|
744
|
+
p_target_type: "runner",
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
catch {
|
|
748
|
+
/* best-effort breadcrumb */
|
|
749
|
+
}
|
|
750
|
+
})();
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* CLAIM-DEADMAN: tear down + FULLY REBUILD the Realtime client — not just a
|
|
754
|
+
* resubscribe. The v0.68 resubscribe / v0.73 watchdog both remove+re-add a
|
|
755
|
+
* channel on the SAME Supabase client, so a wedged underlying socket / Realtime
|
|
756
|
+
* instance survives them (the incident's failure mode). This discards the whole
|
|
757
|
+
* client and builds a fresh one around the same JWT + realtime config (mirrors
|
|
758
|
+
* doRefresh/doFailover's client rebuild, minus the token change), then recovers
|
|
759
|
+
* anything the dead channel missed. Single-flight via `deadmanRebuilding`.
|
|
760
|
+
*/
|
|
761
|
+
export async function rebuildRealtimeClient(state, factory, attempt) {
|
|
762
|
+
if (state.stopped || state.deadmanRebuilding)
|
|
763
|
+
return;
|
|
764
|
+
state.deadmanRebuilding = true;
|
|
765
|
+
try {
|
|
766
|
+
// Tear down the old channel + socket as fully as the client allows.
|
|
767
|
+
try {
|
|
768
|
+
await state.supabase.removeChannel(state.channel);
|
|
769
|
+
}
|
|
770
|
+
catch {
|
|
771
|
+
/* channel may already be dead */
|
|
772
|
+
}
|
|
773
|
+
try {
|
|
774
|
+
await state.supabase.removeAllChannels?.();
|
|
775
|
+
}
|
|
776
|
+
catch {
|
|
777
|
+
/* best-effort */
|
|
778
|
+
}
|
|
779
|
+
try {
|
|
780
|
+
// any-allowed: `.realtime` is not on the narrowed RunnerSupabaseClient type
|
|
781
|
+
// but supabase-js exposes a RealtimeClient with disconnect() here — this is
|
|
782
|
+
// the "fully rebuild, not just resubscribe" teardown of the live socket.
|
|
783
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
784
|
+
state.supabase.realtime?.disconnect?.();
|
|
785
|
+
}
|
|
786
|
+
catch {
|
|
787
|
+
/* best-effort */
|
|
788
|
+
}
|
|
789
|
+
// Fresh client around the current (still-valid) credential.
|
|
790
|
+
state.supabase = createRunnerClient(state.cfg, state.session.access_token, state.session.realtime_config);
|
|
791
|
+
subscribeChannel(state, factory);
|
|
792
|
+
// FLEET-SERVE (wire): hot-swap the chat lane onto the fresh client so chat
|
|
793
|
+
// serving survives the rebuild (mirrors doRefresh/doFailover).
|
|
794
|
+
await state.fleetChat?.refreshClient(state.supabase, state.session.access_token);
|
|
795
|
+
emitDeadmanEvent(state, CLAIM_CHANNEL_REBUILD_VERB, { attempt }, `claim-deadman: rebuilding Realtime client (attempt ${attempt}) — ` +
|
|
796
|
+
`claim channel wedged with a live heartbeat`);
|
|
797
|
+
// Recover anything the wedged channel missed. The seen/seenReviews guards
|
|
798
|
+
// dedupe against a late re-delivery on the fresh channel.
|
|
799
|
+
void reassertRunningClaims(state);
|
|
800
|
+
void pollOnce(state, factory);
|
|
801
|
+
void claimScan(state, factory, "periodic");
|
|
802
|
+
void reviewScan(state, "periodic");
|
|
803
|
+
}
|
|
804
|
+
catch (err) {
|
|
805
|
+
process.stderr.write(`[acc-runner] claim-deadman rebuild failed: ${err.message}\n`);
|
|
806
|
+
}
|
|
807
|
+
finally {
|
|
808
|
+
state.deadmanRebuilding = false;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* CLAIM-DEADMAN: the degraded DB-poll fallback loop. Once the rebuild budget is
|
|
813
|
+
* spent the runner claims by polling the claim path directly — a full epoch-
|
|
814
|
+
* watermark claim scan plus the `seen`-reconcile backstop — every
|
|
815
|
+
* `deadmanFallbackPollMs`. Degraded (no realtime latency) but FUNCTIONAL: tasks
|
|
816
|
+
* get claimed with zero human action while the WS stays dead. Idempotent —
|
|
817
|
+
* starting an already-running loop is a no-op.
|
|
818
|
+
*/
|
|
819
|
+
function startDeadmanFallback(state, factory) {
|
|
820
|
+
if (state.stopped || state.deadmanFallbackTimer)
|
|
821
|
+
return;
|
|
822
|
+
emitDeadmanEvent(state, CLAIM_CHANNEL_FALLBACK_VERB, { poll_ms: state.deadmanFallbackPollMs }, `claim-deadman: Realtime rebuild budget spent — falling back to DB-poll ` +
|
|
823
|
+
`claiming every ${state.deadmanFallbackPollMs}ms (degraded but functional)`);
|
|
824
|
+
const tick = () => {
|
|
825
|
+
void (async () => {
|
|
826
|
+
if (state.stopped)
|
|
827
|
+
return;
|
|
828
|
+
await claimScan(state, factory, "periodic");
|
|
829
|
+
await claimBackstopScan(state, factory);
|
|
830
|
+
})();
|
|
831
|
+
};
|
|
832
|
+
tick(); // claim immediately, don't wait a full interval
|
|
833
|
+
state.deadmanFallbackTimer = setInterval(tick, state.deadmanFallbackPollMs);
|
|
834
|
+
state.deadmanFallbackTimer.unref?.();
|
|
835
|
+
}
|
|
836
|
+
/** CLAIM-DEADMAN: stop the degraded DB-poll fallback loop (on recovery / stop). */
|
|
837
|
+
function stopDeadmanFallback(state) {
|
|
838
|
+
if (state.deadmanFallbackTimer) {
|
|
839
|
+
clearInterval(state.deadmanFallbackTimer);
|
|
840
|
+
state.deadmanFallbackTimer = null;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* CLAIM-DEADMAN: the dead-man evaluation. Reads how much
|
|
845
|
+
* work the server is offering this runner (pending-dispatch — from the heartbeat
|
|
846
|
+
* response when the server supplies it, else the assigned-queued read over the
|
|
847
|
+
* same HTTP path that survives a dead WS), feeds it + the last-claim clock to the
|
|
848
|
+
* pure ClaimDeadman, and acts on its decision:
|
|
849
|
+
*
|
|
850
|
+
* - recovered (offered backlog drained after a wedge episode) → stop the
|
|
851
|
+
* fallback loop + emit `runner.claim_channel_recovered`.
|
|
852
|
+
* - rebuild(N) → full Realtime-client rebuild (rebuildRealtimeClient).
|
|
853
|
+
* - fallback → engage/keep the degraded DB-poll claim loop.
|
|
854
|
+
*
|
|
855
|
+
* A heartbeat failure short-circuits to observeHeartbeatFailure (a network/auth
|
|
856
|
+
* outage is a different failure with its own handling — not a wedge). Bounded
|
|
857
|
+
* and side-effect-safe: never throws out of the loop. Single-flight via
|
|
858
|
+
* `claimDeadmanChecking`.
|
|
859
|
+
*/
|
|
860
|
+
async function claimDeadmanScan(state, factory) {
|
|
861
|
+
if (state.stopped || state.claimDeadmanChecking)
|
|
862
|
+
return;
|
|
863
|
+
state.claimDeadmanChecking = true;
|
|
864
|
+
try {
|
|
865
|
+
if (state.heartbeatFailures > 0) {
|
|
866
|
+
state.claimDeadman.observeHeartbeatFailure();
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
// Only pending work the runner WOULD claim counts as offered — a runner that
|
|
870
|
+
// is deliberately not claiming (paused/quarantined/at-capacity) is not wedged.
|
|
871
|
+
let signal;
|
|
872
|
+
if (deadmanCanClaim(state)) {
|
|
873
|
+
signal = await readDispatchSignal(state.supabase, state.session.runner_id, state.lastHeartbeatDeadmanFields);
|
|
874
|
+
}
|
|
875
|
+
else {
|
|
876
|
+
signal = { pendingDispatch: 0, suspect: false };
|
|
877
|
+
}
|
|
878
|
+
const { recovered } = state.claimDeadman.observeDispatchSignal(signal);
|
|
879
|
+
if (recovered) {
|
|
880
|
+
stopDeadmanFallback(state);
|
|
881
|
+
emitDeadmanEvent(state, CLAIM_CHANNEL_RECOVERED_VERB, {}, `claim-deadman: claim channel recovered — offered backlog drained, ` +
|
|
882
|
+
`runner is claiming normally again`);
|
|
883
|
+
}
|
|
884
|
+
const decision = state.claimDeadman.tick();
|
|
885
|
+
if (decision.wedged) {
|
|
886
|
+
emitDeadmanEvent(state, CLAIM_CHANNEL_WEDGED_VERB, {
|
|
887
|
+
pending_dispatch: decision.pendingDispatch,
|
|
888
|
+
ms_since_claim: decision.msSinceClaim,
|
|
889
|
+
}, `claim-deadman: claim channel WEDGED — ${decision.pendingDispatch} ` +
|
|
890
|
+
`task(s) offered but none claimed for ${Math.round(decision.msSinceClaim / 1000)}s ` +
|
|
891
|
+
`with a live heartbeat; self-healing`);
|
|
892
|
+
}
|
|
893
|
+
if (decision.action === "rebuild") {
|
|
894
|
+
await rebuildRealtimeClient(state, factory, decision.rebuildAttempt ?? 1);
|
|
895
|
+
}
|
|
896
|
+
else if (decision.action === "fallback") {
|
|
897
|
+
startDeadmanFallback(state, factory);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
catch (err) {
|
|
901
|
+
process.stderr.write(`[acc-runner] claim deadman scan failed: ${err.message}\n`);
|
|
902
|
+
}
|
|
903
|
+
finally {
|
|
904
|
+
state.claimDeadmanChecking = false;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
659
907
|
/**
|
|
660
908
|
* v0.65 (T-65-2): full review scan — the review twin of claimScan. Lists EVERY
|
|
661
909
|
* pending review assigned to this runner with an epoch watermark (`1970-…Z`)
|
|
@@ -923,6 +1171,7 @@ export async function watchCommand(options = {}) {
|
|
|
923
1171
|
reviewScanOnce: async () => { },
|
|
924
1172
|
claimWatchdogOnce: async () => { },
|
|
925
1173
|
claimBackstopOnce: async () => { },
|
|
1174
|
+
claimDeadmanOnce: async () => { },
|
|
926
1175
|
gitAuthProbeOnce: async () => false,
|
|
927
1176
|
runningCount: () => 0,
|
|
928
1177
|
};
|
|
@@ -1105,6 +1354,20 @@ export async function watchCommand(options = {}) {
|
|
|
1105
1354
|
claimBackstopTimer: undefined,
|
|
1106
1355
|
claimBackstopIntervalMs: options.claimBackstopIntervalMs ?? DEFAULT_CLAIM_BACKSTOP_INTERVAL_MS,
|
|
1107
1356
|
backstopScanning: false,
|
|
1357
|
+
// CLAIM-DEADMAN: wedge dead-man switch. The pure state
|
|
1358
|
+
// machine carries its own clock (Date.now); tests inject tiny thresholds.
|
|
1359
|
+
claimDeadman: new ClaimDeadman({
|
|
1360
|
+
wedgeThresholdMs: options.claimDeadmanWedgeMs ?? DEFAULT_CLAIM_DEADMAN_WEDGE_MS,
|
|
1361
|
+
rebuildGraceMs: options.claimDeadmanRebuildGraceMs ?? DEFAULT_CLAIM_DEADMAN_REBUILD_GRACE_MS,
|
|
1362
|
+
maxRebuilds: options.claimDeadmanMaxRebuilds ?? DEFAULT_CLAIM_DEADMAN_MAX_REBUILDS,
|
|
1363
|
+
}),
|
|
1364
|
+
claimDeadmanTimer: undefined,
|
|
1365
|
+
claimDeadmanCheckMs: options.claimDeadmanCheckMs ?? DEFAULT_DEADMAN_CHECK_MS,
|
|
1366
|
+
claimDeadmanChecking: false,
|
|
1367
|
+
deadmanRebuilding: false,
|
|
1368
|
+
deadmanFallbackTimer: null,
|
|
1369
|
+
deadmanFallbackPollMs: options.claimDeadmanFallbackPollMs ?? DEFAULT_FALLBACK_POLL_MS,
|
|
1370
|
+
lastHeartbeatDeadmanFields: null,
|
|
1108
1371
|
// GIT-AUTH-ALERT: controller wired below once `state` exists (its emit
|
|
1109
1372
|
// callbacks close over `state`); timer armed in the timer-setup block.
|
|
1110
1373
|
gitAuthController: null,
|
|
@@ -1263,6 +1526,18 @@ export async function watchCommand(options = {}) {
|
|
|
1263
1526
|
state.claimBackstopTimer = setInterval(() => {
|
|
1264
1527
|
void claimBackstopScan(state, taskRunnerFactory);
|
|
1265
1528
|
}, state.claimBackstopIntervalMs);
|
|
1529
|
+
// CLAIM-DEADMAN: the wedge dead-man switch. Escalates
|
|
1530
|
+
// BEYOND the 90s watchdog resubscribe (which only re-adds a channel on the
|
|
1531
|
+
// same client and, per the incident, can itself stop firing): tracks pending-
|
|
1532
|
+
// dispatch vs the last processed claim, and when work is offered but nothing
|
|
1533
|
+
// is claimed for the wedge threshold it FULLY REBUILDS the Realtime client
|
|
1534
|
+
// (twice) then degrades to DB-poll claiming — self-healing the "heartbeat-
|
|
1535
|
+
// alive but claim-dead" runner with zero human action. Independent timer +
|
|
1536
|
+
// single-flight inside claimDeadmanScan().
|
|
1537
|
+
state.claimDeadmanTimer = setInterval(() => {
|
|
1538
|
+
void claimDeadmanScan(state, taskRunnerFactory);
|
|
1539
|
+
}, state.claimDeadmanCheckMs);
|
|
1540
|
+
state.claimDeadmanTimer.unref?.();
|
|
1266
1541
|
// GIT-AUTH-ALERT: periodic git-credential health probe. Armed only when the
|
|
1267
1542
|
// controller is wired (a base clone is resolvable). The same cadence detects
|
|
1268
1543
|
// the first auth failure AND polls for recovery while degraded. Single-flight
|
|
@@ -1368,6 +1643,8 @@ export async function watchCommand(options = {}) {
|
|
|
1368
1643
|
clearInterval(state.reviewScanTimer); // v0.65 (T-65-2)
|
|
1369
1644
|
clearInterval(state.claimWatchdogTimer); // v0.73 (T-73-2)
|
|
1370
1645
|
clearInterval(state.claimBackstopTimer); // FIX-D
|
|
1646
|
+
clearInterval(state.claimDeadmanTimer); // CLAIM-DEADMAN
|
|
1647
|
+
stopDeadmanFallback(state); // CLAIM-DEADMAN
|
|
1371
1648
|
if (state.gitAuthTimer)
|
|
1372
1649
|
clearInterval(state.gitAuthTimer); // GIT-AUTH-ALERT
|
|
1373
1650
|
if (state.resubscribeTimer)
|
|
@@ -1480,6 +1757,7 @@ export async function watchCommand(options = {}) {
|
|
|
1480
1757
|
reviewScanOnce: () => reviewScan(state, "periodic"), // v0.65 (T-65-2)
|
|
1481
1758
|
claimWatchdogOnce: () => claimWatchdogScan(state, taskRunnerFactory), // v0.73 (T-73-2)
|
|
1482
1759
|
claimBackstopOnce: () => claimBackstopScan(state, taskRunnerFactory), // FIX-D
|
|
1760
|
+
claimDeadmanOnce: () => claimDeadmanScan(state, taskRunnerFactory), // CLAIM-DEADMAN
|
|
1483
1761
|
gitAuthProbeOnce: () => gitAuthProbeScan(state), // GIT-AUTH-ALERT
|
|
1484
1762
|
runningCount: () => state.running.size,
|
|
1485
1763
|
};
|
|
@@ -1572,12 +1850,20 @@ function scheduleHeartbeat(state, taskRunnerFactory, baseMs) {
|
|
|
1572
1850
|
const delay = nextHeartbeatDelayMs(state.heartbeatFailures, baseMs);
|
|
1573
1851
|
state.heartbeatTimer = setTimeout(() => {
|
|
1574
1852
|
void (async () => {
|
|
1575
|
-
const { error } = await state.supabase.rpc("heartbeat_runner", {
|
|
1853
|
+
const { data, error } = await state.supabase.rpc("heartbeat_runner", {
|
|
1576
1854
|
p_id: state.session.runner_id,
|
|
1577
1855
|
p_version: PACKAGE_VERSION,
|
|
1578
1856
|
});
|
|
1579
1857
|
if (!error) {
|
|
1580
1858
|
state.heartbeatFailures = 0;
|
|
1859
|
+
// CLAIM-DEADMAN: capture the pending-dispatch count
|
|
1860
|
+
// + suspect flag when the heartbeat RESPONSE carries them, so the runner
|
|
1861
|
+
// learns it is being offered work even with a dead WS. Forward-compatible:
|
|
1862
|
+
// today `heartbeat_runner` returns void and this is a no-op (the deadman
|
|
1863
|
+
// reads the assigned-queued count over HTTP instead). Once the RPC is
|
|
1864
|
+
// extended (Track A migration) this is the authoritative signal — the
|
|
1865
|
+
// deadman prefers it in readDispatchSignal.
|
|
1866
|
+
state.lastHeartbeatDeadmanFields = parseHeartbeatDeadmanFields(data);
|
|
1581
1867
|
// RUNCFG-1: pull the operator's server-set concurrency and apply it to
|
|
1582
1868
|
// the live ceiling within this heartbeat. Runs only on a healthy
|
|
1583
1869
|
// heartbeat (a network/auth failure would fail this RPC too). Awaited so
|
|
@@ -1611,6 +1897,8 @@ function scheduleHeartbeat(state, taskRunnerFactory, baseMs) {
|
|
|
1611
1897
|
clearInterval(state.reviewScanTimer); // v0.65 (T-65-2)
|
|
1612
1898
|
clearInterval(state.claimWatchdogTimer); // v0.73 (T-73-2)
|
|
1613
1899
|
clearInterval(state.claimBackstopTimer); // FIX-D
|
|
1900
|
+
clearInterval(state.claimDeadmanTimer); // CLAIM-DEADMAN
|
|
1901
|
+
stopDeadmanFallback(state); // CLAIM-DEADMAN
|
|
1614
1902
|
if (state.resubscribeTimer)
|
|
1615
1903
|
clearTimeout(state.resubscribeTimer); // v0.68 (T-68-1)
|
|
1616
1904
|
process.exit(1);
|