wicked-crew 0.7.45 → 0.7.47
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/api/capture.d.ts +2 -2
- package/dist/api/delivery-index.d.ts +21 -0
- package/dist/api/delivery-index.d.ts.map +1 -1
- package/dist/api/delivery-index.js +36 -0
- package/dist/api/delivery-index.js.map +1 -1
- package/dist/api/elicitation-cache.d.ts +12 -0
- package/dist/api/elicitation-cache.d.ts.map +1 -1
- package/dist/api/elicitation-cache.js +17 -1
- package/dist/api/elicitation-cache.js.map +1 -1
- package/dist/api/eval-sample.d.ts +6 -6
- package/dist/api/governance-steering.d.ts.map +1 -1
- package/dist/api/governance-steering.js +1 -0
- package/dist/api/governance-steering.js.map +1 -1
- package/dist/api/mcp.d.ts +10 -10
- package/dist/api/policy-landing.d.ts +55 -0
- package/dist/api/policy-landing.d.ts.map +1 -0
- package/dist/api/policy-landing.js +174 -0
- package/dist/api/policy-landing.js.map +1 -0
- package/dist/api/recording.d.ts +228 -0
- package/dist/api/recording.d.ts.map +1 -0
- package/dist/api/recording.js +1140 -0
- package/dist/api/recording.js.map +1 -0
- package/dist/api/roster-standing.d.ts.map +1 -1
- package/dist/api/roster-standing.js +5 -1
- package/dist/api/roster-standing.js.map +1 -1
- package/dist/api/routes.d.ts +31 -39
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +264 -104
- package/dist/api/routes.js.map +1 -1
- package/dist/api/run-timing-index.d.ts +17 -0
- package/dist/api/run-timing-index.d.ts.map +1 -1
- package/dist/api/run-timing-index.js +43 -1
- package/dist/api/run-timing-index.js.map +1 -1
- package/dist/api/seat-health.d.ts +35 -0
- package/dist/api/seat-health.d.ts.map +1 -1
- package/dist/api/seat-health.js +72 -0
- package/dist/api/seat-health.js.map +1 -1
- package/dist/api/seat-standing.d.ts +2 -2
- package/dist/api/seat-standing.d.ts.map +1 -1
- package/dist/api/seat-standing.js +27 -7
- package/dist/api/seat-standing.js.map +1 -1
- package/dist/api/server.d.ts +12 -0
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +74 -1
- package/dist/api/server.js.map +1 -1
- package/dist/api/testing.d.ts +4 -4
- package/dist/api/testing.d.ts.map +1 -1
- package/dist/api/testing.js +3 -0
- package/dist/api/testing.js.map +1 -1
- package/dist/api/watch-routes.d.ts +30 -0
- package/dist/api/watch-routes.d.ts.map +1 -0
- package/dist/api/watch-routes.js +104 -0
- package/dist/api/watch-routes.js.map +1 -0
- package/dist/campaigns/routes.d.ts +2 -2
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +4 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/core/adapter.d.ts +36 -0
- package/dist/core/adapter.d.ts.map +1 -1
- package/dist/core/adapter.js +177 -5
- package/dist/core/adapter.js.map +1 -1
- package/dist/core/run-identity.js +1 -1
- package/dist/core/run-identity.js.map +1 -1
- package/dist/core/walkthrough-root.d.ts +21 -0
- package/dist/core/walkthrough-root.d.ts.map +1 -0
- package/dist/core/walkthrough-root.js +66 -0
- package/dist/core/walkthrough-root.js.map +1 -0
- package/dist/decisions/derive.d.ts +79 -0
- package/dist/decisions/derive.d.ts.map +1 -0
- package/dist/decisions/derive.js +282 -0
- package/dist/decisions/derive.js.map +1 -0
- package/dist/decisions/events.d.ts +23 -0
- package/dist/decisions/events.d.ts.map +1 -0
- package/dist/decisions/events.js +22 -0
- package/dist/decisions/events.js.map +1 -0
- package/dist/decisions/ingest.d.ts +28 -0
- package/dist/decisions/ingest.d.ts.map +1 -0
- package/dist/decisions/ingest.js +92 -0
- package/dist/decisions/ingest.js.map +1 -0
- package/dist/decisions/land.d.ts +100 -0
- package/dist/decisions/land.d.ts.map +1 -0
- package/dist/decisions/land.js +466 -0
- package/dist/decisions/land.js.map +1 -0
- package/dist/decisions/ledger.d.ts +60 -0
- package/dist/decisions/ledger.d.ts.map +1 -0
- package/dist/decisions/ledger.js +246 -0
- package/dist/decisions/ledger.js.map +1 -0
- package/dist/decisions/routes.d.ts +10 -0
- package/dist/decisions/routes.d.ts.map +1 -0
- package/dist/decisions/routes.js +141 -0
- package/dist/decisions/routes.js.map +1 -0
- package/dist/decisions/types.d.ts +120 -0
- package/dist/decisions/types.d.ts.map +1 -0
- package/dist/decisions/types.js +19 -0
- package/dist/decisions/types.js.map +1 -0
- package/dist/interactive/draft-events.d.ts.map +1 -1
- package/dist/interactive/draft-events.js +1 -0
- package/dist/interactive/draft-events.js.map +1 -1
- package/dist/projects/events.d.ts.map +1 -1
- package/dist/projects/events.js +2 -1
- package/dist/projects/events.js.map +1 -1
- package/dist/projects/state-home-preflight.d.ts.map +1 -1
- package/dist/projects/state-home-preflight.js +3 -0
- package/dist/projects/state-home-preflight.js.map +1 -1
- package/dist/projects/state-home-registry.d.ts.map +1 -1
- package/dist/projects/state-home-registry.js +2 -0
- package/dist/projects/state-home-registry.js.map +1 -1
- package/dist/projects/state-home.d.ts +6 -0
- package/dist/projects/state-home.d.ts.map +1 -1
- package/dist/projects/state-home.js +28 -1
- package/dist/projects/state-home.js.map +1 -1
- package/dist/qe/acceptance.d.ts +45 -4
- package/dist/qe/acceptance.d.ts.map +1 -1
- package/dist/qe/acceptance.js +93 -4
- package/dist/qe/acceptance.js.map +1 -1
- package/dist/qe/ledger.d.ts +20 -0
- package/dist/qe/ledger.d.ts.map +1 -1
- package/dist/qe/ledger.js +34 -0
- package/dist/qe/ledger.js.map +1 -1
- package/dist/qe/walkthrough-acceptance.d.ts +82 -0
- package/dist/qe/walkthrough-acceptance.d.ts.map +1 -0
- package/dist/qe/walkthrough-acceptance.js +248 -0
- package/dist/qe/walkthrough-acceptance.js.map +1 -0
- package/dist/skills/base-skill.d.ts.map +1 -1
- package/dist/skills/base-skill.js +3 -2
- package/dist/skills/base-skill.js.map +1 -1
- package/dist/skills/plugin-source.d.ts +19 -1
- package/dist/skills/plugin-source.d.ts.map +1 -1
- package/dist/skills/plugin-source.js +31 -2
- package/dist/skills/plugin-source.js.map +1 -1
- package/dist/skills/runtime.d.ts +11 -2
- package/dist/skills/runtime.d.ts.map +1 -1
- package/dist/skills/runtime.js +29 -1
- package/dist/skills/runtime.js.map +1 -1
- package/dist/skills/store.d.ts +14 -1
- package/dist/skills/store.d.ts.map +1 -1
- package/dist/skills/store.js +19 -5
- package/dist/skills/store.js.map +1 -1
- package/dist/standing-orders/routes.d.ts +4 -4
- package/dist/standing-orders/store.d.ts +2 -2
- package/dist/watch/checks/check-failed.d.ts +24 -0
- package/dist/watch/checks/check-failed.d.ts.map +1 -0
- package/dist/watch/checks/check-failed.js +56 -0
- package/dist/watch/checks/check-failed.js.map +1 -0
- package/dist/watch/checks/deliver-audit.d.ts +19 -0
- package/dist/watch/checks/deliver-audit.d.ts.map +1 -0
- package/dist/watch/checks/deliver-audit.js +129 -0
- package/dist/watch/checks/deliver-audit.js.map +1 -0
- package/dist/watch/checks/index.d.ts +8 -0
- package/dist/watch/checks/index.d.ts.map +1 -0
- package/dist/watch/checks/index.js +19 -0
- package/dist/watch/checks/index.js.map +1 -0
- package/dist/watch/checks/lag.d.ts +24 -0
- package/dist/watch/checks/lag.d.ts.map +1 -0
- package/dist/watch/checks/lag.js +55 -0
- package/dist/watch/checks/lag.js.map +1 -0
- package/dist/watch/checks/quiet-after-claim.d.ts +17 -0
- package/dist/watch/checks/quiet-after-claim.d.ts.map +1 -0
- package/dist/watch/checks/quiet-after-claim.js +89 -0
- package/dist/watch/checks/quiet-after-claim.js.map +1 -0
- package/dist/watch/checks/ungated.d.ts +12 -0
- package/dist/watch/checks/ungated.d.ts.map +1 -0
- package/dist/watch/checks/ungated.js +64 -0
- package/dist/watch/checks/ungated.js.map +1 -0
- package/dist/watch/emit.d.ts +122 -0
- package/dist/watch/emit.d.ts.map +1 -0
- package/dist/watch/emit.js +443 -0
- package/dist/watch/emit.js.map +1 -0
- package/dist/watch/events.d.ts +21 -0
- package/dist/watch/events.d.ts.map +1 -0
- package/dist/watch/events.js +21 -0
- package/dist/watch/events.js.map +1 -0
- package/dist/watch/keys.d.ts +15 -0
- package/dist/watch/keys.d.ts.map +1 -0
- package/dist/watch/keys.js +23 -0
- package/dist/watch/keys.js.map +1 -0
- package/dist/watch/lanes.d.ts +41 -0
- package/dist/watch/lanes.d.ts.map +1 -0
- package/dist/watch/lanes.js +73 -0
- package/dist/watch/lanes.js.map +1 -0
- package/dist/watch/loader.d.ts +47 -0
- package/dist/watch/loader.d.ts.map +1 -0
- package/dist/watch/loader.js +178 -0
- package/dist/watch/loader.js.map +1 -0
- package/dist/watch/registry.d.ts +137 -0
- package/dist/watch/registry.d.ts.map +1 -0
- package/dist/watch/registry.js +530 -0
- package/dist/watch/registry.js.map +1 -0
- package/dist/watch/ring.d.ts +32 -0
- package/dist/watch/ring.d.ts.map +1 -0
- package/dist/watch/ring.js +90 -0
- package/dist/watch/ring.js.map +1 -0
- package/dist/watch/router.d.ts +25 -0
- package/dist/watch/router.d.ts.map +1 -0
- package/dist/watch/router.js +87 -0
- package/dist/watch/router.js.map +1 -0
- package/dist/watch/sources.d.ts +42 -0
- package/dist/watch/sources.d.ts.map +1 -0
- package/dist/watch/sources.js +104 -0
- package/dist/watch/sources.js.map +1 -0
- package/dist/watch/types.d.ts +110 -0
- package/dist/watch/types.d.ts.map +1 -0
- package/dist/watch/types.js +6 -0
- package/dist/watch/types.js.map +1 -0
- package/endpoint-manifest.json +178 -1
- package/package.json +3 -2
- package/watch/entries/deliver-audit.json +17 -0
- package/watch/entries/quiet-after-claim.json +16 -0
- package/watch/entries/registry-check-failed.json +12 -0
- package/watch/entries/registry-lagging.json +12 -0
- package/watch/entries/ungated.json +13 -0
- package/dist/api/demo.d.ts +0 -90
- package/dist/api/demo.d.ts.map +0 -1
- package/dist/api/demo.js +0 -492
- package/dist/api/demo.js.map +0 -1
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `deterministic:ungated` — entry `ungated` (DES-TRIGGER-REGISTRY-001 §4.3 row 2, TR-W5b).
|
|
3
|
+
*
|
|
4
|
+
* A projection of the engine's own `gateEvaluated.ungated` and `ungatedReason` (the entry's filter
|
|
5
|
+
* is empty, so every gate counts toward coverage; only `ungated === true` raises): it computes nothing, and a judge-verified PASS without a floor is gated and
|
|
6
|
+
* never flagged (S5). The engine says such a unit "must be rendered as UNGATED, never as pass", so
|
|
7
|
+
* the flag is a `problem`, never "Finished". `gateEvaluated` carries no attempt; the attempt comes
|
|
8
|
+
* from the unit's `unitOutputCaptured`, emitted before the gate runs (joined).
|
|
9
|
+
*/
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
const REASON_MAX = 200;
|
|
12
|
+
export const ungatedCheck = {
|
|
13
|
+
name: 'deterministic:ungated',
|
|
14
|
+
lane: 'deterministic',
|
|
15
|
+
paramsSchema: z.object({}).strict(),
|
|
16
|
+
thresholdSchema: z.object({}).strict(),
|
|
17
|
+
evaluate(input, state) {
|
|
18
|
+
const bag = state.bag.get('ungated') ?? { attemptByOrd: new Map(), gates: 0, silent: 0 };
|
|
19
|
+
state.bag.set('ungated', bag);
|
|
20
|
+
const e = input.event;
|
|
21
|
+
const ord = typeof e['ord'] === 'number' ? e['ord'] : null;
|
|
22
|
+
if (ord === null)
|
|
23
|
+
return [];
|
|
24
|
+
if (input.type === 'unitOutputCaptured') {
|
|
25
|
+
if (typeof e['attempt'] === 'number')
|
|
26
|
+
bag.attemptByOrd.set(ord, e['attempt']);
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
if (input.type !== 'gateEvaluated')
|
|
30
|
+
return [];
|
|
31
|
+
if (typeof e['ungated'] !== 'boolean') {
|
|
32
|
+
bag.silent++;
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
bag.gates++;
|
|
36
|
+
if (e['ungated'] !== true)
|
|
37
|
+
return [];
|
|
38
|
+
const attempt = bag.attemptByOrd.get(ord) ?? null;
|
|
39
|
+
const raw = typeof e['ungatedReason'] === 'string' ? e['ungatedReason'].trim() : '';
|
|
40
|
+
const reason = raw.length > REASON_MAX ? `${raw.slice(0, REASON_MAX - 1)}…` : raw;
|
|
41
|
+
return [
|
|
42
|
+
{
|
|
43
|
+
op: 'raise',
|
|
44
|
+
subject: `${ord}:${attempt ?? '-'}:ungated`,
|
|
45
|
+
sentence: reason !== '' ? `Nothing checked this step: ${reason}` : 'Nothing checked this step.',
|
|
46
|
+
facts: { ungated_reason: reason },
|
|
47
|
+
ord,
|
|
48
|
+
attempt,
|
|
49
|
+
re: `gateEvaluated#${ord}`,
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
},
|
|
53
|
+
coverage(state) {
|
|
54
|
+
const bag = state.bag.get('ungated');
|
|
55
|
+
if (bag !== undefined && bag.gates > 0)
|
|
56
|
+
return { state: 'checked' };
|
|
57
|
+
if (bag !== undefined && bag.silent > 0) {
|
|
58
|
+
return { state: 'not_checked', reason: 'the engine is too old to say whether a step was checked' };
|
|
59
|
+
}
|
|
60
|
+
return { state: 'not_checked', reason: 'no step has reached its gate yet' };
|
|
61
|
+
},
|
|
62
|
+
describe: () => 'When a step finishes with nothing checking it: no checks, no judge, no policy',
|
|
63
|
+
};
|
|
64
|
+
//# sourceMappingURL=ungated.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ungated.js","sourceRoot":"","sources":["../../../src/watch/checks/ungated.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The emitter and the feed fold (DES-TRIGGER-REGISTRY-001 §4.6-§4.8).
|
|
3
|
+
*
|
|
4
|
+
* EMIT. Outputs are micro-batched (every 2 s, or at 16 waiting) and each row goes to the bus through
|
|
5
|
+
* the engine (`emitOnBus`). A key already on the bus resolves to the existing row: success. A failed
|
|
6
|
+
* emit is retried at 1, 2 and 4 s, then DROPPED with an audit line `watch.emit.failed` and counted
|
|
7
|
+
* in `health.emit.failed` [D-3]: watch rows are advisory, so a lost one is disclosed, never retried
|
|
8
|
+
* forever and never blocking.
|
|
9
|
+
*
|
|
10
|
+
* FOLD. The bus is the one record. The feed is hydrated once at arm from the bus history, then
|
|
11
|
+
* updated only from rows this emitter saw acknowledged, so `GET /watch` never re-reads the bus.
|
|
12
|
+
*
|
|
13
|
+
* RATE. Past an entry's `emit.rate.per_run` rows on one run, a roll-up row stands for the rest: each
|
|
14
|
+
* further output raises the next roll-up row (`rolled_up` = how many it stands for) and clears the
|
|
15
|
+
* previous one with `reason: "rolled_up"`, `replaced_by` the new row (§4.7).
|
|
16
|
+
*/
|
|
17
|
+
import type { WatchFinding, WatchFindingCleared } from 'wicked-crew-api-types';
|
|
18
|
+
import type { CheckOutput, LoadedEntry } from './types.js';
|
|
19
|
+
/** Redacted, capped facts; an over-size object is replaced by a marker rather than cut mid-structure. */
|
|
20
|
+
export declare function scrubFacts(facts: Record<string, unknown> | undefined): Record<string, unknown>;
|
|
21
|
+
/** One feed row: the raised payload, and its clearing once one was acknowledged. */
|
|
22
|
+
export interface FeedRow {
|
|
23
|
+
raised: WatchFinding;
|
|
24
|
+
cleared: WatchFindingCleared | null;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The in-memory feed, BOUNDED (the daemon's bus is never swept, so a lifetime fold would grow
|
|
28
|
+
* without limit): past `maxRows` the oldest cleared row goes first, then the oldest row. The bus
|
|
29
|
+
* stays the record; an evicted row re-raised by a replay resolves to its existing bus row.
|
|
30
|
+
*/
|
|
31
|
+
export declare class WatchFeed {
|
|
32
|
+
private readonly maxRows;
|
|
33
|
+
readonly rows: Map<string, FeedRow>;
|
|
34
|
+
private readonly orphanClears;
|
|
35
|
+
/** Cleared rows in clearing order: the first eviction candidates. */
|
|
36
|
+
private readonly clearedOrder;
|
|
37
|
+
constructor(maxRows?: number);
|
|
38
|
+
get orphanCount(): number;
|
|
39
|
+
private evict;
|
|
40
|
+
has(watchId: string): boolean;
|
|
41
|
+
get(watchId: string): FeedRow | undefined;
|
|
42
|
+
addRaised(f: WatchFinding): void;
|
|
43
|
+
addCleared(c: WatchFindingCleared): void;
|
|
44
|
+
/** Fold one bus row (the boot hydrate). */
|
|
45
|
+
foldBusRow(eventType: string, payload: unknown): void;
|
|
46
|
+
/** Rows of one (run, entry), roll-ups apart (the rate count, rebuilt after a restart). */
|
|
47
|
+
countFor(runId: string | null, entryId: string): {
|
|
48
|
+
plain: number;
|
|
49
|
+
rollups: WatchFinding[];
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export interface EmitterDeps {
|
|
53
|
+
dbPath: string;
|
|
54
|
+
projectOf: (runId: string) => string | undefined;
|
|
55
|
+
/** `audit.record('watch.emit.failed', …)` in the daemon. */
|
|
56
|
+
auditEmitFailed: (detail: Record<string, unknown>) => void;
|
|
57
|
+
/** The proposals sink (§4.11): crew's existing review queue. Never self-applied. */
|
|
58
|
+
submitProposal?: (finding: WatchFinding) => Promise<void>;
|
|
59
|
+
now?: () => number;
|
|
60
|
+
sleep?: (ms: number) => Promise<void>;
|
|
61
|
+
/** Flush cadence, ms (tests pass 0 and call `flush`). */
|
|
62
|
+
flushMs?: number;
|
|
63
|
+
log?: (msg: string) => void;
|
|
64
|
+
/** Called once per acknowledged row (the registry folds nothing else). */
|
|
65
|
+
onAck?: (eventType: string, payload: WatchFinding | WatchFindingCleared) => void;
|
|
66
|
+
}
|
|
67
|
+
export declare class WatchEmitter {
|
|
68
|
+
private readonly deps;
|
|
69
|
+
readonly feed: WatchFeed;
|
|
70
|
+
failed: number;
|
|
71
|
+
proposalsFailed: number;
|
|
72
|
+
private pending;
|
|
73
|
+
private readonly queuedKeys;
|
|
74
|
+
/** Raised rows dropped after their retries (a clearing that depends on one is skipped). */
|
|
75
|
+
private readonly failedKeys;
|
|
76
|
+
/** Raised rows taken out of `pending` and on their way to the bus (a clearing can still find them). */
|
|
77
|
+
private readonly inflight;
|
|
78
|
+
private readonly rate;
|
|
79
|
+
/** The newest watch row on the bus at hydrate: an emit resolving to an id at or below it found an OLD row. */
|
|
80
|
+
private hydratedTail;
|
|
81
|
+
private timer;
|
|
82
|
+
private flushing;
|
|
83
|
+
private readonly now;
|
|
84
|
+
private readonly sleep;
|
|
85
|
+
constructor(deps: EmitterDeps);
|
|
86
|
+
/** Hydrate the feed once from the bus history (§4.8 step 1). */
|
|
87
|
+
hydrate(): Promise<number>;
|
|
88
|
+
start(): void;
|
|
89
|
+
stop(): Promise<void>;
|
|
90
|
+
get waiting(): number;
|
|
91
|
+
private rateOf;
|
|
92
|
+
private envelope;
|
|
93
|
+
/**
|
|
94
|
+
* Queue a raise for `out` (rate and roll-up applied). `replay`: the input came from the boot
|
|
95
|
+
* replay; over the rate such an output is DROPPED rather than rolled up, because the first life
|
|
96
|
+
* may already have counted it and a roll-up row cannot tell (the count may then read low after a
|
|
97
|
+
* restart, never high — a duplicate row is the failure G4 rules out).
|
|
98
|
+
*/
|
|
99
|
+
raise(entry: LoadedEntry, runId: string | null, out: Extract<CheckOutput, {
|
|
100
|
+
op: 'raise';
|
|
101
|
+
}>, anchorAt: number, replay?: boolean): void;
|
|
102
|
+
/** Queue the clearing of the row `subject` names (no-op when it was never raised or is already cleared). */
|
|
103
|
+
clear(entry: LoadedEntry, runId: string | null, subject: string, how: {
|
|
104
|
+
reason: 'resolved';
|
|
105
|
+
} | {
|
|
106
|
+
reason: 'dismissed';
|
|
107
|
+
dismissed_by: string;
|
|
108
|
+
}, re?: string): boolean;
|
|
109
|
+
/** Queue the clearing of `watchId` (dismiss uses this directly). */
|
|
110
|
+
clearById(watchId: string, how: {
|
|
111
|
+
reason: 'resolved';
|
|
112
|
+
} | {
|
|
113
|
+
reason: 'dismissed';
|
|
114
|
+
dismissed_by: string;
|
|
115
|
+
}, re?: string): boolean;
|
|
116
|
+
private push;
|
|
117
|
+
/** Emit everything waiting, in order. Concurrent calls share one pass. */
|
|
118
|
+
flush(): Promise<void>;
|
|
119
|
+
private drain;
|
|
120
|
+
private emitOne;
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=emit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"emit.d.ts","sourceRoot":"","sources":["../../src/watch/emit.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The emitter and the feed fold (DES-TRIGGER-REGISTRY-001 §4.6-§4.8).
|
|
3
|
+
*
|
|
4
|
+
* EMIT. Outputs are micro-batched (every 2 s, or at 16 waiting) and each row goes to the bus through
|
|
5
|
+
* the engine (`emitOnBus`). A key already on the bus resolves to the existing row: success. A failed
|
|
6
|
+
* emit is retried at 1, 2 and 4 s, then DROPPED with an audit line `watch.emit.failed` and counted
|
|
7
|
+
* in `health.emit.failed` [D-3]: watch rows are advisory, so a lost one is disclosed, never retried
|
|
8
|
+
* forever and never blocking.
|
|
9
|
+
*
|
|
10
|
+
* FOLD. The bus is the one record. The feed is hydrated once at arm from the bus history, then
|
|
11
|
+
* updated only from rows this emitter saw acknowledged, so `GET /watch` never re-reads the bus.
|
|
12
|
+
*
|
|
13
|
+
* RATE. Past an entry's `emit.rate.per_run` rows on one run, a roll-up row stands for the rest: each
|
|
14
|
+
* further output raises the next roll-up row (`rolled_up` = how many it stands for) and clears the
|
|
15
|
+
* previous one with `reason: "rolled_up"`, `replaced_by` the new row (§4.7).
|
|
16
|
+
*/
|
|
17
|
+
import { emitOnBus, readBus } from '../core/bus.js';
|
|
18
|
+
import { redactString } from '../mcp/redact.js';
|
|
19
|
+
import { WATCH_BUS_DOMAIN, WATCH_BUS_SUBDOMAIN, WATCH_FINDING_CLEARED, WATCH_FINDING_PREFIX, WATCH_FINDING_RAISED, WATCH_PRODUCER, } from './events.js';
|
|
20
|
+
import { clearedKey, watchIdOf } from './keys.js';
|
|
21
|
+
/** Facts are capped (§4.5): each string redacted and ≤ 200 chars, the whole object ≤ 4 KB. */
|
|
22
|
+
const FACT_STRING_MAX = 200;
|
|
23
|
+
const FACTS_MAX_BYTES = 4096;
|
|
24
|
+
const SENTENCE_MAX = 300;
|
|
25
|
+
const BATCH = 16;
|
|
26
|
+
const RETRY_MS = [1_000, 2_000, 4_000];
|
|
27
|
+
function capString(s, max) {
|
|
28
|
+
const r = redactString(s);
|
|
29
|
+
return r.length > max ? `${r.slice(0, max - 1)}…` : r;
|
|
30
|
+
}
|
|
31
|
+
const FACT_DEPTH_MAX = 6;
|
|
32
|
+
/** Every string redacted and capped at any depth; deeper than the cap, or a cycle, becomes a marker. */
|
|
33
|
+
function scrub(value, depth = 0, seen = new WeakSet()) {
|
|
34
|
+
if (typeof value === 'string')
|
|
35
|
+
return capString(value, FACT_STRING_MAX);
|
|
36
|
+
if (typeof value === 'bigint')
|
|
37
|
+
return value.toString();
|
|
38
|
+
if (value === null || typeof value !== 'object')
|
|
39
|
+
return typeof value === 'function' || typeof value === 'symbol' ? undefined : value;
|
|
40
|
+
if (seen.has(value))
|
|
41
|
+
return '[cycle]';
|
|
42
|
+
if (depth >= FACT_DEPTH_MAX)
|
|
43
|
+
return '[too deep]';
|
|
44
|
+
seen.add(value);
|
|
45
|
+
const out = Array.isArray(value)
|
|
46
|
+
? value.slice(0, 50).map((v) => scrub(v, depth + 1, seen))
|
|
47
|
+
: Object.fromEntries(Object.entries(value).map(([k, v]) => [k, scrub(v, depth + 1, seen)]));
|
|
48
|
+
seen.delete(value);
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
/** Redacted, capped facts; an over-size object is replaced by a marker rather than cut mid-structure. */
|
|
52
|
+
export function scrubFacts(facts) {
|
|
53
|
+
const out = scrub(facts ?? {});
|
|
54
|
+
return Buffer.byteLength(JSON.stringify(out), 'utf8') <= FACTS_MAX_BYTES ? out : { truncated: true };
|
|
55
|
+
}
|
|
56
|
+
/** The in-memory feed: keyed by `watch_id`, plus the (run, entry, subject) → `watch_id` index. */
|
|
57
|
+
/** Rows the feed holds (the routes serve at most 500): the bus stays the full record. */
|
|
58
|
+
const FEED_MAX_ROWS = 20_000;
|
|
59
|
+
/** Rate states held, one per (run, entry); an evicted one is rebuilt from the feed if it recurs. */
|
|
60
|
+
const RATE_MAX = 5_000;
|
|
61
|
+
/**
|
|
62
|
+
* The in-memory feed, BOUNDED (the daemon's bus is never swept, so a lifetime fold would grow
|
|
63
|
+
* without limit): past `maxRows` the oldest cleared row goes first, then the oldest row. The bus
|
|
64
|
+
* stays the record; an evicted row re-raised by a replay resolves to its existing bus row.
|
|
65
|
+
*/
|
|
66
|
+
export class WatchFeed {
|
|
67
|
+
maxRows;
|
|
68
|
+
rows = new Map();
|
|
69
|
+
orphanClears = new Map();
|
|
70
|
+
/** Cleared rows in clearing order: the first eviction candidates. */
|
|
71
|
+
clearedOrder = new Set();
|
|
72
|
+
constructor(maxRows = FEED_MAX_ROWS) {
|
|
73
|
+
this.maxRows = maxRows;
|
|
74
|
+
}
|
|
75
|
+
get orphanCount() {
|
|
76
|
+
return this.orphanClears.size;
|
|
77
|
+
}
|
|
78
|
+
evict() {
|
|
79
|
+
while (this.rows.size > this.maxRows) {
|
|
80
|
+
const victim = this.clearedOrder.values().next().value ?? this.rows.keys().next().value;
|
|
81
|
+
if (victim === undefined)
|
|
82
|
+
return;
|
|
83
|
+
this.rows.delete(victim);
|
|
84
|
+
this.clearedOrder.delete(victim);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
has(watchId) {
|
|
88
|
+
return this.rows.has(watchId);
|
|
89
|
+
}
|
|
90
|
+
get(watchId) {
|
|
91
|
+
return this.rows.get(watchId);
|
|
92
|
+
}
|
|
93
|
+
addRaised(f) {
|
|
94
|
+
if (this.rows.has(f.watch_id))
|
|
95
|
+
return;
|
|
96
|
+
const orphan = this.orphanClears.get(f.watch_id) ?? null;
|
|
97
|
+
this.orphanClears.delete(f.watch_id);
|
|
98
|
+
this.rows.set(f.watch_id, { raised: f, cleared: orphan });
|
|
99
|
+
if (orphan !== null)
|
|
100
|
+
this.clearedOrder.add(f.watch_id);
|
|
101
|
+
this.evict();
|
|
102
|
+
}
|
|
103
|
+
addCleared(c) {
|
|
104
|
+
const row = this.rows.get(c.watch_id);
|
|
105
|
+
if (row === undefined) {
|
|
106
|
+
this.orphanClears.set(c.watch_id, c);
|
|
107
|
+
if (this.orphanClears.size > this.maxRows) {
|
|
108
|
+
const oldest = this.orphanClears.keys().next().value;
|
|
109
|
+
if (oldest !== undefined)
|
|
110
|
+
this.orphanClears.delete(oldest);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else if (row.cleared === null) {
|
|
114
|
+
row.cleared = c;
|
|
115
|
+
this.clearedOrder.add(c.watch_id);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** Fold one bus row (the boot hydrate). */
|
|
119
|
+
foldBusRow(eventType, payload) {
|
|
120
|
+
if (payload === null || typeof payload !== 'object')
|
|
121
|
+
return;
|
|
122
|
+
if (eventType === WATCH_FINDING_RAISED)
|
|
123
|
+
this.addRaised(payload);
|
|
124
|
+
else if (eventType === WATCH_FINDING_CLEARED)
|
|
125
|
+
this.addCleared(payload);
|
|
126
|
+
}
|
|
127
|
+
/** Rows of one (run, entry), roll-ups apart (the rate count, rebuilt after a restart). */
|
|
128
|
+
countFor(runId, entryId) {
|
|
129
|
+
let plain = 0;
|
|
130
|
+
const rollups = [];
|
|
131
|
+
for (const { raised } of this.rows.values()) {
|
|
132
|
+
if (raised.run_id !== runId || raised.entry_id !== entryId)
|
|
133
|
+
continue;
|
|
134
|
+
if (raised.rolled_up > 0)
|
|
135
|
+
rollups.push(raised);
|
|
136
|
+
else
|
|
137
|
+
plain++;
|
|
138
|
+
}
|
|
139
|
+
return { plain, rollups };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
export class WatchEmitter {
|
|
143
|
+
deps;
|
|
144
|
+
feed = new WatchFeed();
|
|
145
|
+
failed = 0;
|
|
146
|
+
proposalsFailed = 0;
|
|
147
|
+
pending = [];
|
|
148
|
+
queuedKeys = new Set();
|
|
149
|
+
/** Raised rows dropped after their retries (a clearing that depends on one is skipped). */
|
|
150
|
+
failedKeys = new Set();
|
|
151
|
+
/** Raised rows taken out of `pending` and on their way to the bus (a clearing can still find them). */
|
|
152
|
+
inflight = new Map();
|
|
153
|
+
rate = new Map();
|
|
154
|
+
/** The newest watch row on the bus at hydrate: an emit resolving to an id at or below it found an OLD row. */
|
|
155
|
+
hydratedTail = 0;
|
|
156
|
+
timer = null;
|
|
157
|
+
flushing = null;
|
|
158
|
+
now;
|
|
159
|
+
sleep;
|
|
160
|
+
constructor(deps) {
|
|
161
|
+
this.deps = deps;
|
|
162
|
+
this.now = deps.now ?? Date.now;
|
|
163
|
+
this.sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms).unref()));
|
|
164
|
+
}
|
|
165
|
+
/** Hydrate the feed once from the bus history (§4.8 step 1). */
|
|
166
|
+
async hydrate() {
|
|
167
|
+
const rows = await readBus(this.deps.dbPath, WATCH_FINDING_PREFIX, { history: true });
|
|
168
|
+
for (const r of rows) {
|
|
169
|
+
this.feed.foldBusRow(r.event_type, r.payload);
|
|
170
|
+
if (r.event_id > this.hydratedTail)
|
|
171
|
+
this.hydratedTail = r.event_id;
|
|
172
|
+
}
|
|
173
|
+
return rows.length;
|
|
174
|
+
}
|
|
175
|
+
start() {
|
|
176
|
+
const ms = this.deps.flushMs ?? 2_000;
|
|
177
|
+
if (ms <= 0 || this.timer !== null)
|
|
178
|
+
return;
|
|
179
|
+
this.timer = setInterval(() => void this.flush(), ms);
|
|
180
|
+
this.timer.unref();
|
|
181
|
+
}
|
|
182
|
+
async stop() {
|
|
183
|
+
if (this.timer !== null)
|
|
184
|
+
clearInterval(this.timer);
|
|
185
|
+
this.timer = null;
|
|
186
|
+
await this.flush();
|
|
187
|
+
}
|
|
188
|
+
get waiting() {
|
|
189
|
+
return this.pending.length;
|
|
190
|
+
}
|
|
191
|
+
rateOf(runId, entryId) {
|
|
192
|
+
const k = `${runId ?? '-'}\u0000${entryId}`;
|
|
193
|
+
let s = this.rate.get(k);
|
|
194
|
+
if (s === undefined) {
|
|
195
|
+
// Rebuilt from the feed after a restart, so the count and the roll-up chain carry on.
|
|
196
|
+
const { plain, rollups } = this.feed.countFor(runId, entryId);
|
|
197
|
+
const rollup = new Map();
|
|
198
|
+
for (const r of rollups) {
|
|
199
|
+
const att = String(r.attempt ?? '-');
|
|
200
|
+
const prev = rollup.get(att);
|
|
201
|
+
if (prev === undefined || r.rolled_up > prev.n)
|
|
202
|
+
rollup.set(att, { n: r.rolled_up, watchId: r.watch_id });
|
|
203
|
+
}
|
|
204
|
+
s = { plain, rollup };
|
|
205
|
+
this.rate.set(k, s);
|
|
206
|
+
if (this.rate.size > RATE_MAX) {
|
|
207
|
+
const oldest = this.rate.keys().next().value;
|
|
208
|
+
if (oldest !== undefined && oldest !== k)
|
|
209
|
+
this.rate.delete(oldest);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return s;
|
|
213
|
+
}
|
|
214
|
+
envelope(entry, runId, ord, attempt, re) {
|
|
215
|
+
const projectId = runId !== null ? this.deps.projectOf(runId) : undefined;
|
|
216
|
+
return {
|
|
217
|
+
run_id: runId,
|
|
218
|
+
ord,
|
|
219
|
+
attempt,
|
|
220
|
+
by: `watch:${entry.id}@${entry.version}`,
|
|
221
|
+
at: this.now(),
|
|
222
|
+
re,
|
|
223
|
+
entry_id: entry.id,
|
|
224
|
+
entry_version: entry.version,
|
|
225
|
+
...(projectId !== undefined ? { project_id: projectId } : {}),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Queue a raise for `out` (rate and roll-up applied). `replay`: the input came from the boot
|
|
230
|
+
* replay; over the rate such an output is DROPPED rather than rolled up, because the first life
|
|
231
|
+
* may already have counted it and a roll-up row cannot tell (the count may then read low after a
|
|
232
|
+
* restart, never high — a duplicate row is the failure G4 rules out).
|
|
233
|
+
*/
|
|
234
|
+
raise(entry, runId, out, anchorAt, replay = false) {
|
|
235
|
+
const watchId = watchIdOf(runId, entry.id, entry.version, out.subject);
|
|
236
|
+
if (this.feed.has(watchId) || this.queuedKeys.has(watchId))
|
|
237
|
+
return; // a replay: already the record
|
|
238
|
+
const rate = this.rateOf(runId, entry.id);
|
|
239
|
+
const ord = out.ord ?? null;
|
|
240
|
+
const attempt = out.attempt ?? null;
|
|
241
|
+
const base = (subject, rolledUp, sentence) => ({
|
|
242
|
+
...this.envelope(entry, runId, ord, attempt, out.re),
|
|
243
|
+
watch_id: watchIdOf(runId, entry.id, entry.version, subject),
|
|
244
|
+
check: entry.check,
|
|
245
|
+
kind: out.kind ?? entry.emit.as,
|
|
246
|
+
severity: out.severity ?? entry.emit.severity,
|
|
247
|
+
watch_kind: entry.emit.watch_kind,
|
|
248
|
+
attach: entry.emit.attach,
|
|
249
|
+
sentence: capString(sentence, SENTENCE_MAX),
|
|
250
|
+
facts: scrubFacts(out.facts),
|
|
251
|
+
anchor: runId !== null ? { run_id: runId, ord, attempt, at: anchorAt } : null,
|
|
252
|
+
evidence: out.evidence ?? [],
|
|
253
|
+
model: null,
|
|
254
|
+
rolled_up: rolledUp,
|
|
255
|
+
});
|
|
256
|
+
if (rate.plain < entry.emit.rate.per_run) {
|
|
257
|
+
rate.plain++;
|
|
258
|
+
this.push(WATCH_FINDING_RAISED, base(out.subject, 0, out.sentence), entry.emit.as === 'proposal', { plainOf: rate });
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (replay)
|
|
262
|
+
return;
|
|
263
|
+
// Over the rate: the next roll-up row for this attempt replaces the previous one.
|
|
264
|
+
const att = String(attempt ?? '-');
|
|
265
|
+
const prev = rate.rollup.get(att);
|
|
266
|
+
const n = (prev?.n ?? 0) + 1;
|
|
267
|
+
const row = base(`rollup:${att}#${n}`, n, `${n} more like this on this run (latest: ${out.sentence})`);
|
|
268
|
+
rate.rollup.set(att, { n, watchId: row.watch_id });
|
|
269
|
+
// Coalesce a burst: while the previous roll-up row has not reached the bus yet, it is REPLACED
|
|
270
|
+
// in the batch (its clearing would only name a row nobody ever saw), and a waiting clearing
|
|
271
|
+
// that pointed at it now points at the new row.
|
|
272
|
+
const waiting = prev === undefined ? -1 : this.pending.findIndex((p) => p.eventType === WATCH_FINDING_RAISED && p.key === prev.watchId);
|
|
273
|
+
if (prev !== undefined && waiting !== -1) {
|
|
274
|
+
const replaced = this.pending[waiting];
|
|
275
|
+
this.queuedKeys.delete(prev.watchId);
|
|
276
|
+
this.queuedKeys.add(row.watch_id);
|
|
277
|
+
this.failedKeys.delete(row.watch_id);
|
|
278
|
+
// The chain falls back to where the REPLACED row would have restored it.
|
|
279
|
+
this.pending[waiting] = {
|
|
280
|
+
eventType: WATCH_FINDING_RAISED,
|
|
281
|
+
key: row.watch_id,
|
|
282
|
+
payload: row,
|
|
283
|
+
proposal: false,
|
|
284
|
+
restore: { rate, att, to: replaced.restore?.to },
|
|
285
|
+
};
|
|
286
|
+
for (const p of this.pending) {
|
|
287
|
+
const c = p.payload;
|
|
288
|
+
if (p.eventType === WATCH_FINDING_CLEARED && c.reason === 'rolled_up' && c.replaced_by === prev.watchId) {
|
|
289
|
+
c.replaced_by = row.watch_id;
|
|
290
|
+
p.dependsOn = row.watch_id;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
this.push(WATCH_FINDING_RAISED, row, false, { restore: { rate, att, to: prev } });
|
|
296
|
+
if (prev !== undefined) {
|
|
297
|
+
this.push(WATCH_FINDING_CLEARED, {
|
|
298
|
+
...this.envelope(entry, runId, ord, attempt, out.re),
|
|
299
|
+
watch_id: prev.watchId,
|
|
300
|
+
reason: 'rolled_up',
|
|
301
|
+
replaced_by: row.watch_id,
|
|
302
|
+
}, false, { dependsOn: row.watch_id });
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/** Queue the clearing of the row `subject` names (no-op when it was never raised or is already cleared). */
|
|
306
|
+
clear(entry, runId, subject, how, re = 'resolved') {
|
|
307
|
+
const watchId = watchIdOf(runId, entry.id, entry.version, subject);
|
|
308
|
+
return this.clearById(watchId, how, re);
|
|
309
|
+
}
|
|
310
|
+
/** Queue the clearing of `watchId` (dismiss uses this directly). */
|
|
311
|
+
clearById(watchId, how, re = 'resolved') {
|
|
312
|
+
const row = this.feed.get(watchId);
|
|
313
|
+
const queuedRaise = this.pending.find((p) => p.eventType === WATCH_FINDING_RAISED && p.payload.watch_id === watchId);
|
|
314
|
+
const unacked = queuedRaise?.payload ?? this.inflight.get(watchId);
|
|
315
|
+
const raised = row?.raised ?? unacked;
|
|
316
|
+
if (raised === undefined || (row !== undefined && row.cleared !== null) || this.queuedKeys.has(clearedKey(watchId)))
|
|
317
|
+
return false;
|
|
318
|
+
const payload = {
|
|
319
|
+
run_id: raised.run_id,
|
|
320
|
+
ord: raised.ord,
|
|
321
|
+
attempt: raised.attempt,
|
|
322
|
+
by: raised.by,
|
|
323
|
+
at: this.now(),
|
|
324
|
+
re,
|
|
325
|
+
watch_id: watchId,
|
|
326
|
+
entry_id: raised.entry_id,
|
|
327
|
+
entry_version: raised.entry_version,
|
|
328
|
+
...(raised.project_id !== undefined ? { project_id: raised.project_id } : {}),
|
|
329
|
+
...how,
|
|
330
|
+
};
|
|
331
|
+
// Not yet acknowledged: the clearing waits on its raise, and is skipped if that raise is dropped.
|
|
332
|
+
this.push(WATCH_FINDING_CLEARED, payload, false, row === undefined ? { dependsOn: watchId } : {});
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
push(eventType, payload, proposal, extra = {}) {
|
|
336
|
+
const key = eventType === WATCH_FINDING_RAISED ? payload.watch_id : clearedKey(payload.watch_id);
|
|
337
|
+
this.queuedKeys.add(key);
|
|
338
|
+
if (eventType === WATCH_FINDING_RAISED)
|
|
339
|
+
this.failedKeys.delete(key);
|
|
340
|
+
this.pending.push({ eventType, key, payload, proposal, ...extra });
|
|
341
|
+
if (this.pending.length >= BATCH)
|
|
342
|
+
void this.flush();
|
|
343
|
+
}
|
|
344
|
+
/** Emit everything waiting, in order. Concurrent calls share one pass. */
|
|
345
|
+
flush() {
|
|
346
|
+
if (this.flushing !== null)
|
|
347
|
+
return this.flushing.then(() => (this.pending.length > 0 ? this.flush() : undefined));
|
|
348
|
+
this.flushing = this.drain().finally(() => {
|
|
349
|
+
this.flushing = null;
|
|
350
|
+
});
|
|
351
|
+
return this.flushing;
|
|
352
|
+
}
|
|
353
|
+
async drain() {
|
|
354
|
+
while (this.pending.length > 0) {
|
|
355
|
+
const batch = this.pending.splice(0, BATCH);
|
|
356
|
+
for (const p of batch)
|
|
357
|
+
if (p.eventType === WATCH_FINDING_RAISED)
|
|
358
|
+
this.inflight.set(p.key, p.payload);
|
|
359
|
+
for (const p of batch) {
|
|
360
|
+
try {
|
|
361
|
+
await this.emitOne(p);
|
|
362
|
+
}
|
|
363
|
+
finally {
|
|
364
|
+
this.inflight.delete(p.key);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
async emitOne(p) {
|
|
370
|
+
if (p.dependsOn !== undefined && this.failedKeys.has(p.dependsOn)) {
|
|
371
|
+
// Its raised row never reached the bus: a clearing pointing at it would name nothing.
|
|
372
|
+
this.queuedKeys.delete(p.key);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const idempotency = p.eventType === WATCH_FINDING_RAISED ? raisedKeyOf(p.payload) : clearedKey(p.payload.watch_id);
|
|
376
|
+
const wasNew = p.eventType === WATCH_FINDING_RAISED && !this.feed.has(p.payload.watch_id);
|
|
377
|
+
let lastErr = null;
|
|
378
|
+
let eventId = 0;
|
|
379
|
+
for (let i = 0; i <= RETRY_MS.length; i++) {
|
|
380
|
+
try {
|
|
381
|
+
eventId = await emitOnBus(this.deps.dbPath, {
|
|
382
|
+
event_type: p.eventType,
|
|
383
|
+
domain: WATCH_BUS_DOMAIN,
|
|
384
|
+
subdomain: WATCH_BUS_SUBDOMAIN,
|
|
385
|
+
payload: p.payload,
|
|
386
|
+
producer_id: WATCH_PRODUCER,
|
|
387
|
+
idempotency_key: idempotency,
|
|
388
|
+
});
|
|
389
|
+
lastErr = null;
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
catch (err) {
|
|
393
|
+
lastErr = err;
|
|
394
|
+
if (i < RETRY_MS.length)
|
|
395
|
+
await this.sleep(RETRY_MS[i]);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
this.queuedKeys.delete(p.key);
|
|
399
|
+
if (lastErr !== null) {
|
|
400
|
+
this.failed++;
|
|
401
|
+
if (p.eventType === WATCH_FINDING_RAISED) {
|
|
402
|
+
this.failedKeys.add(p.key);
|
|
403
|
+
// Give back what the dropped row took: its rate slot, or its place at the head of the chain.
|
|
404
|
+
if (p.plainOf !== undefined)
|
|
405
|
+
p.plainOf.plain = Math.max(0, p.plainOf.plain - 1);
|
|
406
|
+
if (p.restore !== undefined && p.restore.rate.rollup.get(p.restore.att)?.watchId === p.key) {
|
|
407
|
+
if (p.restore.to === undefined)
|
|
408
|
+
p.restore.rate.rollup.delete(p.restore.att);
|
|
409
|
+
else
|
|
410
|
+
p.restore.rate.rollup.set(p.restore.att, p.restore.to);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
this.deps.auditEmitFailed({
|
|
414
|
+
event_type: p.eventType,
|
|
415
|
+
watch_id: p.payload.watch_id,
|
|
416
|
+
entry_id: p.payload.entry_id,
|
|
417
|
+
error: lastErr instanceof Error ? lastErr.message : String(lastErr),
|
|
418
|
+
});
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (p.eventType === WATCH_FINDING_RAISED)
|
|
422
|
+
this.feed.addRaised(p.payload);
|
|
423
|
+
else
|
|
424
|
+
this.feed.addCleared(p.payload);
|
|
425
|
+
this.deps.onAck?.(p.eventType, p.payload);
|
|
426
|
+
// An emit that resolved to a row older than this boot's hydrate (one the bounded feed had
|
|
427
|
+
// evicted) is not new: its proposal was filed when it was first raised.
|
|
428
|
+
if (p.proposal && wasNew && eventId > this.hydratedTail && this.deps.submitProposal !== undefined) {
|
|
429
|
+
try {
|
|
430
|
+
await this.deps.submitProposal(p.payload);
|
|
431
|
+
}
|
|
432
|
+
catch (err) {
|
|
433
|
+
this.proposalsFailed++;
|
|
434
|
+
this.deps.log?.(`[watch] proposal for ${p.payload.watch_id} not filed: ${err instanceof Error ? err.message : String(err)}`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
/** The raised row's idempotency key: `watch_id` is `"w-"` + the key (keys.ts `watchIdOf`). */
|
|
440
|
+
function raisedKeyOf(f) {
|
|
441
|
+
return f.watch_id.slice(2);
|
|
442
|
+
}
|
|
443
|
+
//# sourceMappingURL=emit.js.map
|