forge-workflow 0.1.0-beta.4 → 0.1.0-beta.6
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/AGENTS.md +18 -7
- package/CHANGELOG.md +79 -1
- package/CLAUDE.md +0 -12
- package/CODING_STANDARDS.md +72 -0
- package/README.md +6 -2
- package/bin/forge-cmd.js +20 -0
- package/bin/forge.js +28 -375
- package/docs/INDEX.md +1 -1
- package/docs/guides/BEADS_GITHUB_SYNC.md +2 -31
- package/docs/guides/MIGRATION.md +4 -4
- package/docs/guides/SETUP.md +16 -16
- package/docs/reference/COMMANDS.md +8 -5
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +4 -0
- package/docs/reference/INSIGHTS_RECAP.md +9 -20
- package/docs/reference/INSTALL.md +4 -0
- package/docs/reference/LEGACY_CLAIM_REPAIR.md +112 -0
- package/docs/reference/RELEASE.md +5 -3
- package/docs/reference/TOOLCHAIN.md +8 -0
- package/docs/reference/github-accounts.md +134 -0
- package/docs/reference/protected-state-surfaces.md +4 -4
- package/docs/reference/shepherd.md +114 -35
- package/lefthook.yml +12 -0
- package/lib/activation/ensure-forge-home.js +33 -15
- package/lib/adapters/pr-state-adapter.js +359 -144
- package/lib/audit-evidence.js +71 -110
- package/lib/base-remote.js +138 -0
- package/lib/beta5-compatibility-evidence.js +1093 -0
- package/lib/bun-lockfile-proof.js +413 -0
- package/lib/bun-workflow-pins.js +461 -0
- package/lib/capabilities/index.js +9 -0
- package/lib/capabilities/model.js +141 -0
- package/lib/capabilities/probes.js +347 -0
- package/lib/capped-jsonl-log.js +236 -0
- package/lib/codex-skills.js +2 -2
- package/lib/commands/_manifest.js +1 -0
- package/lib/commands/_registry.js +50 -20
- package/lib/commands/clean.js +252 -32
- package/lib/commands/dev.js +4 -33
- package/lib/commands/doctor.js +37 -6
- package/lib/commands/gate.js +197 -27
- package/lib/commands/github.js +215 -0
- package/lib/commands/hooks.js +276 -30
- package/lib/commands/insights.js +8 -3
- package/lib/commands/memory.js +66 -2
- package/lib/commands/merge.js +1265 -58
- package/lib/commands/plan.js +33 -2
- package/lib/commands/pr.js +3 -1
- package/lib/commands/preflight.js +21 -4
- package/lib/commands/prime.js +21 -8
- package/lib/commands/push.js +146 -54
- package/lib/commands/recall.js +127 -49
- package/lib/commands/recap.js +6 -1
- package/lib/commands/release.js +39 -3
- package/lib/commands/remember.js +28 -4
- package/lib/commands/serve.js +26 -9
- package/lib/commands/setup.js +323 -98
- package/lib/commands/shepherd.js +591 -73
- package/lib/commands/ship.js +36 -91
- package/lib/commands/skill.js +127 -11
- package/lib/commands/status.js +17 -1
- package/lib/commands/team.js +47 -8
- package/lib/commands/test.js +187 -38
- package/lib/commands/validate.js +65 -21
- package/lib/commands/worktree.js +359 -45
- package/lib/core/runtime-graph.js +1 -1
- package/lib/doc-assertions.js +297 -0
- package/lib/existing-tdd-gate.js +253 -0
- package/lib/fixtures/beta5-corpus/v1/README.md +9 -0
- package/lib/fixtures/beta5-corpus/v1/contract/command-contract.json +26 -0
- package/lib/fixtures/beta5-corpus/v1/contract/package-contract.json +13 -0
- package/lib/fixtures/beta5-corpus/v1/contract/workflow-stage-matrix.json +8 -0
- package/lib/fixtures/beta5-corpus/v1/manifest.json +25 -0
- package/lib/fixtures/beta5-corpus/v1/state/comments.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/config.yaml +6 -0
- package/lib/fixtures/beta5-corpus/v1/state/dependencies.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/issues.jsonl +2 -0
- package/lib/fixtures/beta5-corpus/v1/state/kernel.sql +20 -0
- package/lib/forge-context.js +1 -4
- package/lib/forge-issues.js +134 -32
- package/lib/gate-events.js +98 -10
- package/lib/git-defaults.js +56 -0
- package/lib/github-context.js +308 -0
- package/lib/global-flags.js +1 -0
- package/lib/harness-capability-matrix.js +3 -3
- package/lib/hook-renderer.js +122 -5
- package/lib/insights.js +96 -80
- package/lib/issue-render.js +19 -0
- package/lib/kernel/backing-issue.js +14 -2
- package/lib/kernel/broker.js +739 -31
- package/lib/kernel/claim-reconciler.js +238 -0
- package/lib/kernel/cli-broker-factory.js +12 -1
- package/lib/kernel/close-on-merge.js +154 -0
- package/lib/kernel/fs-class.js +42 -25
- package/lib/kernel/lease-enforcer.js +9 -4
- package/lib/kernel/legacy-claim-repair.js +442 -0
- package/lib/kernel/live-claim-projection.js +26 -0
- package/lib/kernel/migrations.js +118 -3
- package/lib/kernel/readiness-model.js +184 -12
- package/lib/kernel/schema.js +49 -1
- package/lib/kernel/sqlite-driver.js +3435 -172
- package/lib/kernel/taxonomy-validator.js +4 -1
- package/lib/kernel/windows-private-acl.js +239 -0
- package/lib/lefthook-wiring.js +21 -1
- package/lib/memory/hygiene.js +191 -0
- package/lib/memory/router.js +110 -28
- package/lib/memory/usage-evidence.js +4 -0
- package/lib/memory-digest.js +106 -15
- package/lib/memory-recall-events.js +145 -0
- package/lib/memory-recall.js +71 -10
- package/lib/merge-rules.js +143 -21
- package/lib/npm-publish-workflow.js +465 -0
- package/lib/orientation.js +68 -43
- package/lib/package-root.js +2 -0
- package/lib/plugin-catalog.js +14 -4
- package/lib/pr-bundle.js +5 -6
- package/lib/pr-monitor/auto-actions.js +169 -28
- package/lib/pr-monitor/differ.js +110 -4
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/flow-monitor.js +1424 -0
- package/lib/pr-monitor/gather.js +251 -44
- package/lib/pr-monitor/journal.js +18 -39
- package/lib/pr-monitor/monitor.js +117 -10
- package/lib/pr-monitor/process-identity.js +117 -0
- package/lib/pr-monitor/reconcile-executor.js +1129 -470
- package/lib/pr-monitor/reconcile.js +0 -0
- package/lib/pr-monitor/render-summary.js +293 -0
- package/lib/pr-monitor/review-preflight.js +269 -0
- package/lib/pr-monitor/shepherd-lease.js +38 -20
- package/lib/pr-monitor/verdict.js +438 -0
- package/lib/pr-monitor/watch-lifecycle.js +145 -27
- package/lib/pr-monitor/watch-owner.js +1414 -0
- package/lib/pr-monitor/watch.js +129 -58
- package/lib/pr-pull.js +33 -14
- package/lib/pr-shepherd.js +51 -11
- package/lib/preflight/gates.js +65 -18
- package/lib/preflight/runner.js +5 -0
- package/lib/project-memory.js +178 -4
- package/lib/protected-state-authority.js +1100 -0
- package/lib/protected-state-surfaces.js +243 -45
- package/lib/release-readiness.js +53 -7
- package/lib/review-adapter.js +65 -0
- package/lib/shell-utils.js +1 -1
- package/lib/skills-sync.js +71 -35
- package/lib/smart-merge.js +28 -4
- package/lib/symlink-utils.js +74 -26
- package/lib/upgrade-safety.js +39 -0
- package/lib/using-forge.js +19 -6
- package/lib/validation/risk-manifest.js +339 -0
- package/lib/workflow/enforce-stage.js +44 -0
- package/lib/workflow/plan-authority.js +225 -0
- package/package.json +12 -9
- package/scripts/commitlint.js +13 -15
- package/scripts/doc-asserting-tests.js +158 -0
- package/scripts/generate-risk-manifest.js +91 -0
- package/scripts/github-context-bridge.sh +10 -0
- package/scripts/legacy-claim-repair.js +145 -0
- package/scripts/lib/behavioral-eval-runner.js +310 -0
- package/scripts/lib/behavioral-eval-runtime.js +457 -0
- package/scripts/lib/eval-evidence.js +328 -0
- package/scripts/lib/eval-runner.js +81 -41
- package/scripts/lib/immutable-eval-corpus.js +309 -0
- package/scripts/lib/promotion-evidence-loader.js +94 -0
- package/scripts/lib/promotion-scorecard.js +314 -0
- package/scripts/npm-release-receipt.js +134 -0
- package/scripts/process-tree.js +773 -0
- package/scripts/protected-state-check.js +479 -31
- package/scripts/run-command-eval.js +29 -1
- package/scripts/sync-agent-skills.js +333 -34
- package/scripts/sync-d20-audit.js +172 -0
- package/scripts/test-full-suite.js +935 -37
- package/scripts/test-profile.js +13 -3
- package/scripts/test.js +271 -57
- package/skills/coverage.json +1 -0
- package/skills/review/SKILL.md +6 -11
- package/skills/review/evals/scorecard.json +4 -4
- package/skills/rollback/SKILL.md +4 -11
- package/skills/rollback/evals/scorecard.json +3 -3
- package/skills/setup/SKILL.md +18 -0
- package/skills/setup/evals/scorecard.json +3 -3
- package/skills/shepherd/SKILL.md +39 -16
- package/skills/shepherd/evals/scorecard.json +4 -4
- package/skills/ship/SKILL.md +4 -12
- package/skills/ship/evals/scorecard.json +3 -3
- package/skills/validate/SKILL.md +3 -0
- package/skills/validate/evals/scorecard.json +1 -1
- package/skills/worktree/SKILL.md +6 -1
- package/skills/worktree/evals/scorecard.json +2 -2
- package/lib/beads-setup.js +0 -538
- package/lib/beads-sync-scaffold.js +0 -189
- package/lib/pat-setup.js +0 -207
- package/lib/pr-monitor/render-sticky.js +0 -206
- package/lib/pr-monitor/upsert-sticky.js +0 -169
- package/scripts/beads-context.sh +0 -577
- package/scripts/beads-migrate-to-dolt.sh +0 -7
- package/scripts/beads-upgrade-smoke.sh +0 -284
- package/scripts/lib/beads-migrate-to-dolt.mjs +0 -503
|
@@ -0,0 +1,1424 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const zlib = require('node:zlib');
|
|
5
|
+
|
|
6
|
+
const { computeContentHash, validateContractStructure } = require('../../packages/memory-contracts');
|
|
7
|
+
const {
|
|
8
|
+
MonitorRuntimeError,
|
|
9
|
+
createMonitorDurabilityBridge,
|
|
10
|
+
createMonitorReceipt,
|
|
11
|
+
createMonitorState,
|
|
12
|
+
reduceMonitor,
|
|
13
|
+
reduceMonitorBatch,
|
|
14
|
+
} = require('../../packages/flow');
|
|
15
|
+
const { diffSnapshots } = require('./differ');
|
|
16
|
+
const { EVENT_TYPES: T, makeEvent } = require('./events');
|
|
17
|
+
const { finalizeRecords, MAX_EVENT_DELTAS } = require('./monitor');
|
|
18
|
+
|
|
19
|
+
const MAX_HISTORY = 128;
|
|
20
|
+
const DELIVERY_TARGET = 'legacy-journal';
|
|
21
|
+
const SNAPSHOT_TEXT_LIMIT = 64;
|
|
22
|
+
const SNAPSHOT_OBJECT_KEY_LIMIT = 24;
|
|
23
|
+
const JOB_URL_LIMIT = 2048;
|
|
24
|
+
const PENDING_BATCH_LIMIT = 12_000;
|
|
25
|
+
const PENDING_BATCH_OUTPUT_LIMIT = 262_144;
|
|
26
|
+
// One event per bounded surface delta, plus a second reply/resolution event per thread.
|
|
27
|
+
const PENDING_BATCH_RECORD_LIMIT = 538;
|
|
28
|
+
const BATCH_PLAN_FORMAT = 'segments-v1';
|
|
29
|
+
const BATCH_PLAN_SEGMENT_LIMIT = 128;
|
|
30
|
+
const MAX_REOPEN_DEPTH = 8;
|
|
31
|
+
const SNAPSHOT_FORMAT = 'digest-v1';
|
|
32
|
+
const SNAPSHOT_EVIDENCE_KEYS = new Set(['checks', 'threads', 'reviews', 'comments', 'degraded']);
|
|
33
|
+
const UNSAFE_PROVIDER_DIAGNOSTIC = /(?:gh[pousr]_[a-z0-9]{20,}|github_pat_[a-z0-9_]{20,}|sk_(?:live|test)_[a-z0-9]{16,}|sk-[a-z0-9]{16,}|AKIA[0-9A-Z]{16}|(?:api[_-]?key|token|secret|password)\s*[:=]\s*\S{8,}|(?:^|[\\/])(?:users|home|root)[\\/]\S+)/i;
|
|
34
|
+
const REDACTED_PROVIDER_DIAGNOSTIC = '[provider diagnostic redacted]';
|
|
35
|
+
const PRIVATE_MONITOR_SCAN_LIMIT = 16_384;
|
|
36
|
+
|
|
37
|
+
class FlowMonitorError extends Error {
|
|
38
|
+
constructor(code, message, options) {
|
|
39
|
+
super(message, options);
|
|
40
|
+
this.name = 'FlowMonitorError';
|
|
41
|
+
this.code = code;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function validBatchPlanSegmentCount(value) {
|
|
46
|
+
return Number.isSafeInteger(value) && value > 1 && value <= BATCH_PLAN_SEGMENT_LIMIT;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function appendPendingBatch(batches, batch) {
|
|
50
|
+
if (batches.length >= BATCH_PLAN_SEGMENT_LIMIT) {
|
|
51
|
+
throw new FlowMonitorError('INVALID_OBSERVATION', 'Pending monitor transition plan exceeds its segment bound');
|
|
52
|
+
}
|
|
53
|
+
batches.push(batch);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function encodePrivateSafeBase64(value) {
|
|
57
|
+
const substitutions = { '-': '.', _: '~', A: '!', a: '*' };
|
|
58
|
+
return value.toString('base64url').replace(/[-_Aa]/g, character => substitutions[character]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function decodePrivateSafeBase64(value) {
|
|
62
|
+
const substitutions = { '.': '-', '~': '_', '!': 'A', '*': 'a' };
|
|
63
|
+
return Buffer.from(value.replace(/[.~!*]/g, character => substitutions[character]), 'base64url');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function defaultNow() { return new Date().toISOString(); }
|
|
67
|
+
|
|
68
|
+
function boundSnapshotScalar(value, depth = 0) {
|
|
69
|
+
if (typeof value === 'string') return safeProviderDiagnostic(value).slice(0, SNAPSHOT_TEXT_LIMIT);
|
|
70
|
+
if (value === null || typeof value !== 'object') return value;
|
|
71
|
+
if (depth >= 4) return null;
|
|
72
|
+
if (Array.isArray(value)) {
|
|
73
|
+
return value.slice(0, SNAPSHOT_OBJECT_KEY_LIMIT)
|
|
74
|
+
.map(item => boundSnapshotScalar(item, depth + 1));
|
|
75
|
+
}
|
|
76
|
+
return Object.fromEntries(Object.entries(value)
|
|
77
|
+
.slice(0, SNAPSHOT_OBJECT_KEY_LIMIT)
|
|
78
|
+
.map(([key, item]) => [boundIdentity(key), boundSnapshotScalar(item, depth + 1)]));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function identityToken(value) {
|
|
82
|
+
return crypto.createHash('sha256').update(String(value ?? '')).digest('base64url').slice(0, 12);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function boundIdentity(value) {
|
|
86
|
+
if (typeof value !== 'string'
|
|
87
|
+
|| (value.length <= SNAPSHOT_TEXT_LIMIT && !unsafeProviderString(value))) return value;
|
|
88
|
+
return `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function privacySafeIdentity(value) {
|
|
92
|
+
return unsafeProviderString(value) ? boundIdentity(value) : value;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function validBoundedJobUrl(value) {
|
|
96
|
+
if (typeof value !== 'string' || value.length > JOB_URL_LIMIT || unsafeProviderString(value)) return null;
|
|
97
|
+
try {
|
|
98
|
+
const url = new URL(value);
|
|
99
|
+
return (url.protocol === 'https:' || url.protocol === 'http:')
|
|
100
|
+
&& !url.username && !url.password && !url.search && !url.hash
|
|
101
|
+
? value
|
|
102
|
+
: null;
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function safeProviderDiagnostic(value) {
|
|
109
|
+
return unsafeProviderString(value)
|
|
110
|
+
? REDACTED_PROVIDER_DIAGNOSTIC
|
|
111
|
+
: value;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function unsafeProviderString(value) {
|
|
115
|
+
return typeof value === 'string'
|
|
116
|
+
&& (value.length > PRIVATE_MONITOR_SCAN_LIMIT || UNSAFE_PROVIDER_DIAGNOSTIC.test(value));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function boundMonitorRecord(record) {
|
|
120
|
+
const safeError = safeProviderDiagnostic(record.data?.error);
|
|
121
|
+
const safeRecord = safeError !== record.data?.error
|
|
122
|
+
? { ...record, data: { ...record.data, error: safeError } }
|
|
123
|
+
: record;
|
|
124
|
+
const bounded = boundSnapshotScalar(safeRecord);
|
|
125
|
+
bounded.key = boundIdentity(record.key);
|
|
126
|
+
for (const field of ['name', 'threadId', 'author', 'commitOid', 'surface']) {
|
|
127
|
+
if (typeof record.data?.[field] === 'string') bounded.data[field] = boundIdentity(record.data[field]);
|
|
128
|
+
}
|
|
129
|
+
const jobUrl = validBoundedJobUrl(record.data?.jobUrl);
|
|
130
|
+
if (jobUrl) bounded.data.jobUrl = jobUrl;
|
|
131
|
+
else if (bounded.data) delete bounded.data.jobUrl;
|
|
132
|
+
return bounded;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function durableMonitorRecord(record) {
|
|
136
|
+
return {
|
|
137
|
+
...boundMonitorRecord(record),
|
|
138
|
+
...(typeof record.repo === 'string' ? { repo: privacySafeIdentity(record.repo) } : {}),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function encodePendingBatch(ctx, subjectRevision, records, snapshot) {
|
|
143
|
+
if (records.length < 1 || records.length > PENDING_BATCH_RECORD_LIMIT) {
|
|
144
|
+
throw new FlowMonitorError('INVALID_OBSERVATION', 'Pending monitor transition batch exceeds its record bound');
|
|
145
|
+
}
|
|
146
|
+
const core = {
|
|
147
|
+
monitor_id: ctx.monitorId,
|
|
148
|
+
subject_revision: subjectRevision,
|
|
149
|
+
snapshot_identity: snapshot === null ? null : snapshotIdentity(snapshot),
|
|
150
|
+
records: records.map(durableMonitorRecord),
|
|
151
|
+
snapshot,
|
|
152
|
+
};
|
|
153
|
+
const serialized = JSON.stringify({
|
|
154
|
+
...core,
|
|
155
|
+
batch_id: computeContentHash(core),
|
|
156
|
+
});
|
|
157
|
+
if (Buffer.byteLength(serialized) > PENDING_BATCH_OUTPUT_LIMIT) {
|
|
158
|
+
throw new FlowMonitorError('INVALID_OBSERVATION', 'Pending monitor transition batch exceeds its recovery bound');
|
|
159
|
+
}
|
|
160
|
+
const encoded = encodePrivateSafeBase64(zlib.gzipSync(serialized, { level: 9 }));
|
|
161
|
+
if (encoded.length > PENDING_BATCH_LIMIT) {
|
|
162
|
+
throw new FlowMonitorError('INVALID_OBSERVATION', 'Pending monitor transition batch exceeds its durable bound');
|
|
163
|
+
}
|
|
164
|
+
return encoded;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function decodePendingBatch(value, ctx, subjectRevision) {
|
|
168
|
+
if (typeof value !== 'string' || value.length > PENDING_BATCH_LIMIT) {
|
|
169
|
+
throw new FlowMonitorError('MONITOR_HISTORY_INCOMPLETE', 'Pending monitor transition batch is missing');
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
const decoded = JSON.parse(zlib.gunzipSync(decodePrivateSafeBase64(value), {
|
|
173
|
+
maxOutputLength: PENDING_BATCH_OUTPUT_LIMIT,
|
|
174
|
+
}));
|
|
175
|
+
const { batch_id: batchId, ...core } = decoded || {};
|
|
176
|
+
if (core.monitor_id !== ctx.monitorId || core.subject_revision !== subjectRevision
|
|
177
|
+
|| core.snapshot_identity !== (core.snapshot === null ? null : snapshotIdentity(core.snapshot))
|
|
178
|
+
|| batchId !== computeContentHash(core)
|
|
179
|
+
|| !Array.isArray(core.records) || core.records.length < 1
|
|
180
|
+
|| core.records.length > PENDING_BATCH_RECORD_LIMIT) {
|
|
181
|
+
throw new Error('invalid pending batch');
|
|
182
|
+
}
|
|
183
|
+
return { ...core, batchId, encoded: value };
|
|
184
|
+
} catch (error) {
|
|
185
|
+
if (error instanceof FlowMonitorError) throw error;
|
|
186
|
+
throw new FlowMonitorError('MONITOR_HISTORY_INCOMPLETE', 'Pending monitor transition batch is malformed');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function segmentablePendingBatchError(error) {
|
|
191
|
+
return error instanceof FlowMonitorError
|
|
192
|
+
&& error.code === 'INVALID_OBSERVATION'
|
|
193
|
+
&& [
|
|
194
|
+
'Pending monitor transition batch exceeds its recovery bound',
|
|
195
|
+
'Pending monitor transition batch exceeds its durable bound',
|
|
196
|
+
].includes(error.message);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function splitPendingBatches(ctx, subjectRevision, records, snapshot) {
|
|
200
|
+
const batches = [];
|
|
201
|
+
let offset = 0;
|
|
202
|
+
while (offset < records.length) {
|
|
203
|
+
try {
|
|
204
|
+
const encoded = encodePendingBatch(ctx, subjectRevision, records.slice(offset), snapshot);
|
|
205
|
+
appendPendingBatch(batches, { encoded, records: records.slice(offset), snapshot });
|
|
206
|
+
break;
|
|
207
|
+
} catch (error) {
|
|
208
|
+
if (!segmentablePendingBatchError(error)) throw error;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let low = 1;
|
|
212
|
+
let high = records.length - offset - 1;
|
|
213
|
+
let accepted = null;
|
|
214
|
+
while (low <= high) {
|
|
215
|
+
const count = Math.floor((low + high) / 2);
|
|
216
|
+
const candidate = records.slice(offset, offset + count);
|
|
217
|
+
try {
|
|
218
|
+
accepted = {
|
|
219
|
+
count,
|
|
220
|
+
encoded: encodePendingBatch(ctx, subjectRevision, candidate, null),
|
|
221
|
+
records: candidate,
|
|
222
|
+
snapshot: null,
|
|
223
|
+
};
|
|
224
|
+
low = count + 1;
|
|
225
|
+
} catch (error) {
|
|
226
|
+
if (!segmentablePendingBatchError(error)) throw error;
|
|
227
|
+
high = count - 1;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (!accepted) {
|
|
231
|
+
throw new FlowMonitorError('INVALID_OBSERVATION', 'Pending monitor transition record exceeds its durable bound');
|
|
232
|
+
}
|
|
233
|
+
appendPendingBatch(batches, accepted);
|
|
234
|
+
offset += accepted.count;
|
|
235
|
+
}
|
|
236
|
+
return batches;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function batchPlanReference(planId, startSequence, segmentCount, extra = {}) {
|
|
240
|
+
return {
|
|
241
|
+
format: BATCH_PLAN_FORMAT,
|
|
242
|
+
id: planId,
|
|
243
|
+
start_sequence: startSequence,
|
|
244
|
+
segment_count: segmentCount,
|
|
245
|
+
...extra,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function validBatchPlanReference(value) {
|
|
250
|
+
return value && typeof value === 'object'
|
|
251
|
+
&& value.format === BATCH_PLAN_FORMAT
|
|
252
|
+
&& typeof value.id === 'string' && /^[a-f0-9]{64}$/.test(value.id)
|
|
253
|
+
&& Number.isSafeInteger(value.start_sequence) && value.start_sequence > 0
|
|
254
|
+
&& validBatchPlanSegmentCount(value.segment_count);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function batchPlanSegmentIndex(batches, recordOffset) {
|
|
258
|
+
let boundary = 0;
|
|
259
|
+
for (let index = 0; index < batches.length; index += 1) {
|
|
260
|
+
boundary += batches[index].records.length;
|
|
261
|
+
if (recordOffset < boundary) return index;
|
|
262
|
+
}
|
|
263
|
+
return -1;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function reopenedMonitorId(monitorId, terminalReceiptId) {
|
|
267
|
+
const lifecycleSeed = `${monitorId}:${terminalReceiptId}`;
|
|
268
|
+
const lifecycleHash = crypto.createHash('sha256').update(lifecycleSeed).digest('hex');
|
|
269
|
+
return `pr-reopen:${lifecycleHash}`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function compactSnapshot(value) {
|
|
273
|
+
if (value?._snapshotFormat === SNAPSHOT_FORMAT) return value;
|
|
274
|
+
const source = value && typeof value === 'object' ? value : {};
|
|
275
|
+
const scalarSource = Object.fromEntries(Object.entries(source)
|
|
276
|
+
.filter(([key]) => !SNAPSHOT_EVIDENCE_KEYS.has(key)));
|
|
277
|
+
const compact = boundSnapshotScalar(scalarSource);
|
|
278
|
+
const evidence = {
|
|
279
|
+
checks: (Array.isArray(source.checks) ? source.checks : [])
|
|
280
|
+
.map(item => [identityToken(item?.name), boundSnapshotScalar(item?.class)]),
|
|
281
|
+
threads: (Array.isArray(source.threads) ? source.threads : [])
|
|
282
|
+
.map(item => [identityToken(item?.threadId), Boolean(item?.isResolved), item?.commentCount || 0, Boolean(item?.actionable)]),
|
|
283
|
+
reviews: (Array.isArray(source.reviews) ? source.reviews : [])
|
|
284
|
+
.map(item => [identityToken(item?.author), identityToken(item?.submittedAt), identityToken(item?.commitOid)]),
|
|
285
|
+
comments: (Array.isArray(source.comments) ? source.comments : [])
|
|
286
|
+
.map(item => [identityToken(item?.id)]),
|
|
287
|
+
degraded: (Array.isArray(source.degraded) ? source.degraded : [])
|
|
288
|
+
.map(item => [identityToken(item?.surface)]),
|
|
289
|
+
};
|
|
290
|
+
const evidenceJson = JSON.stringify(evidence);
|
|
291
|
+
const durableScalar = {
|
|
292
|
+
...compact,
|
|
293
|
+
...(typeof source.repo === 'string' ? { repo: privacySafeIdentity(source.repo) } : {}),
|
|
294
|
+
_snapshotFormat: SNAPSHOT_FORMAT,
|
|
295
|
+
};
|
|
296
|
+
return {
|
|
297
|
+
...durableScalar,
|
|
298
|
+
_snapshotIdentity: crypto.createHash('sha256')
|
|
299
|
+
.update(JSON.stringify({ snapshot: durableScalar, evidence }))
|
|
300
|
+
.digest('hex'),
|
|
301
|
+
_snapshotEvidence: encodePrivateSafeBase64(zlib.gzipSync(evidenceJson, { level: 9 })),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function snapshotIdentity(snapshot) {
|
|
306
|
+
return typeof snapshot?._snapshotIdentity === 'string'
|
|
307
|
+
? snapshot._snapshotIdentity
|
|
308
|
+
: computeContentHash(snapshot);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function comparableSnapshot(compact, source = null) {
|
|
312
|
+
if (compact?._snapshotFormat !== SNAPSHOT_FORMAT) return compact;
|
|
313
|
+
let evidence;
|
|
314
|
+
try {
|
|
315
|
+
evidence = JSON.parse(zlib.gunzipSync(decodePrivateSafeBase64(compact._snapshotEvidence)));
|
|
316
|
+
} catch {
|
|
317
|
+
throw new FlowMonitorError('MONITOR_HISTORY_INCOMPLETE', 'Durable monitor snapshot evidence is malformed');
|
|
318
|
+
}
|
|
319
|
+
const tupleLengths = { checks: 2, threads: 4, reviews: 3, comments: 1, degraded: 1 };
|
|
320
|
+
const valid = evidence && typeof evidence === 'object' && !Array.isArray(evidence)
|
|
321
|
+
&& Object.entries(tupleLengths).every(([key, length]) => Array.isArray(evidence[key])
|
|
322
|
+
&& evidence[key].every(item => Array.isArray(item) && item.length === length));
|
|
323
|
+
if (!valid) {
|
|
324
|
+
throw new FlowMonitorError('MONITOR_HISTORY_INCOMPLETE', 'Durable monitor snapshot evidence is incomplete');
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
...compact,
|
|
328
|
+
checks: evidence.checks.map((item, index) => ({
|
|
329
|
+
name: item[0], class: item[1], displayName: source?.checks?.[index]?.name,
|
|
330
|
+
})),
|
|
331
|
+
threads: evidence.threads.map((item, index) => ({
|
|
332
|
+
threadId: item[0], isResolved: item[1], commentCount: item[2], actionable: item[3],
|
|
333
|
+
path: boundSnapshotScalar(source?.threads?.[index]?.path),
|
|
334
|
+
})),
|
|
335
|
+
reviews: evidence.reviews.map((item, index) => ({
|
|
336
|
+
author: item[0], submittedAt: item[1], commitOid: item[2],
|
|
337
|
+
state: boundSnapshotScalar(source?.reviews?.[index]?.state),
|
|
338
|
+
displayAuthor: boundSnapshotScalar(source?.reviews?.[index]?.author),
|
|
339
|
+
displayCommitOid: boundSnapshotScalar(source?.reviews?.[index]?.commitOid),
|
|
340
|
+
})),
|
|
341
|
+
comments: evidence.comments.map((item, index) => ({
|
|
342
|
+
id: item[0], author: boundSnapshotScalar(source?.comments?.[index]?.author),
|
|
343
|
+
})),
|
|
344
|
+
degraded: evidence.degraded.map((item, index) => ({
|
|
345
|
+
surface: item[0],
|
|
346
|
+
displaySurface: boundSnapshotScalar(source?.degraded?.[index]?.surface),
|
|
347
|
+
error: boundSnapshotScalar(safeProviderDiagnostic(source?.degraded?.[index]?.error)),
|
|
348
|
+
})),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function uuidFrom(seed) {
|
|
353
|
+
const hex = crypto.createHash('sha256').update(seed).digest('hex').slice(0, 32).split('');
|
|
354
|
+
hex[12] = '4';
|
|
355
|
+
hex[16] = ['8', '9', 'a', 'b'][Number.parseInt(hex[16], 16) % 4];
|
|
356
|
+
return `${hex.slice(0, 8).join('')}-${hex.slice(8, 12).join('')}-${hex.slice(12, 16).join('')}-${hex.slice(16, 20).join('')}-${hex.slice(20).join('')}`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function envelope(schemaId, objectId, createdAt, producerId, payload) {
|
|
360
|
+
const value = {
|
|
361
|
+
schema_id: schemaId,
|
|
362
|
+
schema_version: 1,
|
|
363
|
+
object_id: objectId,
|
|
364
|
+
created_at: createdAt,
|
|
365
|
+
producer: { product_id: 'forge', product_version: '0.1.0', instance_id: producerId },
|
|
366
|
+
capabilities_used: [],
|
|
367
|
+
provenance: { source_kind: 'monitor', actor_class: 'system', actor_id: producerId },
|
|
368
|
+
payload,
|
|
369
|
+
extensions: {},
|
|
370
|
+
};
|
|
371
|
+
value.content_hash = computeContentHash(value);
|
|
372
|
+
return value;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function parseEventEnvelope(row) {
|
|
376
|
+
let event;
|
|
377
|
+
try {
|
|
378
|
+
event = typeof row?.envelope_json === 'string' ? JSON.parse(row.envelope_json) : row;
|
|
379
|
+
} catch {
|
|
380
|
+
throw new FlowMonitorError('MONITOR_HISTORY_INCOMPLETE', 'Durable monitor history is malformed');
|
|
381
|
+
}
|
|
382
|
+
if (typeof row?.content_hash === 'string' && row.content_hash !== event?.content_hash) {
|
|
383
|
+
throw new FlowMonitorError('MONITOR_HISTORY_INCOMPLETE', 'Durable monitor history is incomplete');
|
|
384
|
+
}
|
|
385
|
+
return event;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function parseEventRow(row, monitorId) {
|
|
389
|
+
const event = parseEventEnvelope(row);
|
|
390
|
+
const validation = validateContractStructure(event);
|
|
391
|
+
if (!validation.ok
|
|
392
|
+
|| event?.schema_id !== 'forge.memory.monitor-event.v1'
|
|
393
|
+
|| event.payload?.monitor_id !== monitorId
|
|
394
|
+
|| !Number.isSafeInteger(event.payload?.sequence)) {
|
|
395
|
+
throw new FlowMonitorError('MONITOR_HISTORY_INCOMPLETE', 'Durable monitor history is incomplete');
|
|
396
|
+
}
|
|
397
|
+
return event;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function parseAuthorityTailRow(row) {
|
|
401
|
+
return parseEventEnvelope(row);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function persistenceRow(event) {
|
|
405
|
+
return {
|
|
406
|
+
event_id: event.payload.event_id,
|
|
407
|
+
monitor_id: event.payload.monitor_id,
|
|
408
|
+
sequence: event.payload.sequence,
|
|
409
|
+
content_hash: event.content_hash,
|
|
410
|
+
envelope_json: JSON.stringify(event),
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function bridgeStore(store) {
|
|
415
|
+
return {
|
|
416
|
+
appendEvent: (...args) => store.appendEvent(...args),
|
|
417
|
+
recordDeliveryReceipt: (...args) => store.recordDeliveryReceipt(...args),
|
|
418
|
+
recordTerminalReceipt: (...args) => {
|
|
419
|
+
if (typeof store.recordTerminalReceipt !== 'function') {
|
|
420
|
+
throw new FlowMonitorError('PROVIDER_UNAVAILABLE', 'Terminal receipt store is unavailable');
|
|
421
|
+
}
|
|
422
|
+
return store.recordTerminalReceipt(...args);
|
|
423
|
+
},
|
|
424
|
+
async getEvent(eventId, config) {
|
|
425
|
+
const row = await store.getEvent(eventId, config);
|
|
426
|
+
if (!row) return row;
|
|
427
|
+
let event = row;
|
|
428
|
+
if (typeof row.envelope_json === 'string') {
|
|
429
|
+
try {
|
|
430
|
+
event = JSON.parse(row.envelope_json);
|
|
431
|
+
} catch {
|
|
432
|
+
throw new FlowMonitorError('MONITOR_HISTORY_INCOMPLETE', 'Durable monitor history is malformed');
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return row.monitor_id ? row : persistenceRow(event);
|
|
436
|
+
},
|
|
437
|
+
readEventTail: (...args) => store.readEventTail(...args),
|
|
438
|
+
readDeliveryState: (...args) => store.readDeliveryState(...args),
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function monitorSpec(ctx, subjectRevision) {
|
|
443
|
+
return {
|
|
444
|
+
monitorId: ctx.monitorId,
|
|
445
|
+
ownerRunId: ctx.ownerRunId,
|
|
446
|
+
packetId: ctx.packetId,
|
|
447
|
+
subject: { id: ctx.subjectId, revision: subjectRevision },
|
|
448
|
+
sourceAdapters: ['github-pr'],
|
|
449
|
+
deliveryTargets: [DELIVERY_TARGET],
|
|
450
|
+
lifetime: 'subject',
|
|
451
|
+
deadline: '9999-12-31T23:59:59.999Z',
|
|
452
|
+
maxPending: MAX_HISTORY,
|
|
453
|
+
maxRetries: 3,
|
|
454
|
+
retryBaseMs: 1000,
|
|
455
|
+
maxHistory: MAX_HISTORY,
|
|
456
|
+
securityPolicy: { maxPayloadBytes: 16_384 },
|
|
457
|
+
reducer: (_previous, observation, event) => ({
|
|
458
|
+
sequence: event.payload.sequence,
|
|
459
|
+
snapshot: observation.snapshot,
|
|
460
|
+
}),
|
|
461
|
+
terminalPredicate: () => false,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function monitorEventId(monitorId, sequence, type, key) {
|
|
466
|
+
return crypto.createHash('sha256')
|
|
467
|
+
.update(`${monitorId}:${sequence}:${type}:${key}`)
|
|
468
|
+
.digest('hex');
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function monitorEvent(
|
|
472
|
+
ctx,
|
|
473
|
+
record,
|
|
474
|
+
snapshot,
|
|
475
|
+
subjectRevision,
|
|
476
|
+
checkpointComplete = true,
|
|
477
|
+
pendingBatch = null,
|
|
478
|
+
recordIsBounded = false,
|
|
479
|
+
batchPlan = null,
|
|
480
|
+
) {
|
|
481
|
+
const seed = `${ctx.monitorId}:${record.seq}:${record.type}:${record.key}`;
|
|
482
|
+
const boundedRecord = recordIsBounded ? record : durableMonitorRecord(record);
|
|
483
|
+
return envelope(
|
|
484
|
+
'forge.memory.monitor-event.v1',
|
|
485
|
+
uuidFrom(`event-object:${seed}`),
|
|
486
|
+
record.ts,
|
|
487
|
+
ctx.monitorId,
|
|
488
|
+
{
|
|
489
|
+
monitor_id: ctx.monitorId,
|
|
490
|
+
event_id: monitorEventId(ctx.monitorId, record.seq, record.type, record.key),
|
|
491
|
+
sequence: record.seq,
|
|
492
|
+
subject_revision: subjectRevision,
|
|
493
|
+
type: record.type,
|
|
494
|
+
actionability: record.type === T.SNAPSHOT_CHECKPOINT ? 'advisory' : 'action_required',
|
|
495
|
+
observed_at: record.ts,
|
|
496
|
+
bounded_payload: {
|
|
497
|
+
record: boundedRecord,
|
|
498
|
+
snapshot,
|
|
499
|
+
checkpoint_complete: checkpointComplete,
|
|
500
|
+
...(pendingBatch ? { pending_batch: pendingBatch } : {}),
|
|
501
|
+
...(batchPlan ? { batch_plan: batchPlan } : {}),
|
|
502
|
+
},
|
|
503
|
+
},
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function deliveryReceipt(ctx, event, deliveredAt) {
|
|
508
|
+
return envelope(
|
|
509
|
+
'forge.memory.delivery-receipt.v1',
|
|
510
|
+
uuidFrom(`delivery:${event.payload.event_id}:${DELIVERY_TARGET}`),
|
|
511
|
+
deliveredAt,
|
|
512
|
+
ctx.monitorId,
|
|
513
|
+
{
|
|
514
|
+
event_id: event.payload.event_id,
|
|
515
|
+
target: DELIVERY_TARGET,
|
|
516
|
+
transport_tier: 'T1',
|
|
517
|
+
attempt: 1,
|
|
518
|
+
delivered_at: deliveredAt,
|
|
519
|
+
acknowledged: true,
|
|
520
|
+
outcome: 'acknowledged',
|
|
521
|
+
},
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function terminalPrState(snapshot) {
|
|
526
|
+
const state = String(snapshot?.prState || snapshot?.state || '').toUpperCase();
|
|
527
|
+
if (state === 'MERGED') return 'PASS';
|
|
528
|
+
if (state === 'CLOSED') return 'FAIL';
|
|
529
|
+
return null;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async function recordTerminalHandoff(ctx, bridge, spec, state, snapshot, createdAt) {
|
|
533
|
+
const terminalState = terminalPrState(snapshot);
|
|
534
|
+
if (!terminalState) return null;
|
|
535
|
+
const terminating = reduceMonitor(spec, state, { kind: 'subject-terminal', terminalState }).state;
|
|
536
|
+
const terminal = reduceMonitor(spec, terminating, {
|
|
537
|
+
kind: 'cleanup-complete',
|
|
538
|
+
processCleanup: { status: 'not-required', owner: 'repo-singleton-shepherd' },
|
|
539
|
+
leaseCleanup: { status: 'checkpointed', continuing_authority: false },
|
|
540
|
+
}).state;
|
|
541
|
+
const receipt = createMonitorReceipt(spec, terminal, {
|
|
542
|
+
objectId: uuidFrom(`terminal:${ctx.monitorId}:${terminalState}:${terminal.lastSequence}`),
|
|
543
|
+
createdAt,
|
|
544
|
+
producerInstanceId: ctx.monitorId,
|
|
545
|
+
});
|
|
546
|
+
await bridge.recordTerminalReceipt(receipt, { maxHistory: MAX_HISTORY });
|
|
547
|
+
return receipt.object_id;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async function readAuthority(ctx, spec) {
|
|
551
|
+
let tail;
|
|
552
|
+
try {
|
|
553
|
+
tail = await ctx.store.readEventTail(ctx.monitorId, { limit: MAX_HISTORY });
|
|
554
|
+
} catch (error) {
|
|
555
|
+
throw new FlowMonitorError('PROVIDER_UNAVAILABLE', 'Durable monitor history is unavailable', { cause: error });
|
|
556
|
+
}
|
|
557
|
+
if (!tail || !Array.isArray(tail.events) || tail.events.length > MAX_HISTORY
|
|
558
|
+
|| (tail.overflow && tail.events.length !== MAX_HISTORY)) {
|
|
559
|
+
throw new FlowMonitorError('MONITOR_HISTORY_INCOMPLETE', 'Durable monitor history exceeds its restart bound');
|
|
560
|
+
}
|
|
561
|
+
const events = tail.events.map(parseAuthorityTailRow);
|
|
562
|
+
let state;
|
|
563
|
+
try {
|
|
564
|
+
state = reduceMonitorBatch(
|
|
565
|
+
spec,
|
|
566
|
+
createMonitorState(spec),
|
|
567
|
+
events.map(event => ({ kind: 'observation', event })),
|
|
568
|
+
).state;
|
|
569
|
+
} catch (error) {
|
|
570
|
+
if (error instanceof MonitorRuntimeError) {
|
|
571
|
+
throw new FlowMonitorError('MONITOR_HISTORY_INCOMPLETE', 'Durable monitor history is incomplete', { cause: error });
|
|
572
|
+
}
|
|
573
|
+
throw error;
|
|
574
|
+
}
|
|
575
|
+
const firstSequence = events[0]?.payload.sequence ?? 1;
|
|
576
|
+
if (tail.overflow && tail.truncated_before_sequence !== firstSequence) {
|
|
577
|
+
throw new FlowMonitorError('MONITOR_HISTORY_INCOMPLETE', 'Durable monitor checkpoint boundary is invalid');
|
|
578
|
+
}
|
|
579
|
+
for (let index = 0; index < events.length; index += 1) {
|
|
580
|
+
if (events[index].payload.sequence !== firstSequence + index) {
|
|
581
|
+
throw new FlowMonitorError('MONITOR_HISTORY_INCOMPLETE', 'Durable monitor history is not contiguous');
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return { events, state };
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async function readDeliveryAuthority(ctx) {
|
|
588
|
+
let deliveryState;
|
|
589
|
+
try {
|
|
590
|
+
deliveryState = await ctx.store.readDeliveryState(ctx.monitorId, { limit: MAX_HISTORY });
|
|
591
|
+
} catch {
|
|
592
|
+
throw new FlowMonitorError('PROVIDER_UNAVAILABLE', 'Durable delivery state is unavailable');
|
|
593
|
+
}
|
|
594
|
+
if (!deliveryState || deliveryState.overflow?.cursors || !Array.isArray(deliveryState.cursors)) {
|
|
595
|
+
throw new FlowMonitorError('PROVIDER_UNAVAILABLE', 'Durable delivery state is incomplete');
|
|
596
|
+
}
|
|
597
|
+
return deliveryState;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function terminalReplay(deliveryState, monitorId) {
|
|
601
|
+
if (!deliveryState.terminal_receipt) return null;
|
|
602
|
+
let terminal;
|
|
603
|
+
try {
|
|
604
|
+
terminal = JSON.parse(deliveryState.terminal_receipt.envelope_json);
|
|
605
|
+
} catch {
|
|
606
|
+
throw new FlowMonitorError('PROVIDER_UNAVAILABLE', 'Durable terminal receipt is malformed');
|
|
607
|
+
}
|
|
608
|
+
const validation = validateContractStructure(terminal);
|
|
609
|
+
if (!validation.ok
|
|
610
|
+
|| terminal?.schema_id !== 'forge.memory.monitor-receipt.v1'
|
|
611
|
+
|| terminal.payload?.monitor_id !== monitorId
|
|
612
|
+
|| typeof terminal.object_id !== 'string' || !terminal.object_id
|
|
613
|
+
|| deliveryState.terminal_receipt.content_hash !== terminal.content_hash) {
|
|
614
|
+
throw new FlowMonitorError('PROVIDER_UNAVAILABLE', 'Durable terminal receipt is incomplete');
|
|
615
|
+
}
|
|
616
|
+
return {
|
|
617
|
+
events: [], changed: false, authority: 'memory',
|
|
618
|
+
terminalReceiptId: terminal.object_id,
|
|
619
|
+
receiptIds: [terminal.object_id],
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
async function redeliverOutstanding(
|
|
624
|
+
ctx,
|
|
625
|
+
bridge,
|
|
626
|
+
spec,
|
|
627
|
+
initialState,
|
|
628
|
+
durableEvents,
|
|
629
|
+
deliveryState,
|
|
630
|
+
now,
|
|
631
|
+
recordLimit = MAX_EVENT_DELTAS,
|
|
632
|
+
) {
|
|
633
|
+
let state = initialState;
|
|
634
|
+
const receiptIds = [];
|
|
635
|
+
const cursor = deliveryState.cursors.find(item => item.target === DELIVERY_TARGET)?.sequence ?? 0;
|
|
636
|
+
const firstSequence = durableEvents[0]?.payload.sequence ?? 1;
|
|
637
|
+
const lastSequence = durableEvents.at(-1)?.payload.sequence ?? 0;
|
|
638
|
+
if (cursor < firstSequence - 1 || cursor > lastSequence) {
|
|
639
|
+
throw new FlowMonitorError('MONITOR_HISTORY_INCOMPLETE', 'Durable delivery cursor is outside the restart checkpoint');
|
|
640
|
+
}
|
|
641
|
+
const cursorEvent = durableEvents.find(event => event.payload.sequence === cursor);
|
|
642
|
+
if (cursorEvent && cursorEvent.payload.actionability !== 'advisory' && cursor > 0) {
|
|
643
|
+
state = reduceMonitor(spec, state, { kind: 'acknowledge', sequence: cursor }).state;
|
|
644
|
+
}
|
|
645
|
+
const outstanding = durableEvents.filter(item => item.payload.sequence > cursor);
|
|
646
|
+
for (const event of outstanding.slice(0, recordLimit)) {
|
|
647
|
+
if (event.payload.actionability !== 'advisory') {
|
|
648
|
+
await ctx.deliverLegacy(event.payload.bounded_payload.record, event.payload.event_id);
|
|
649
|
+
}
|
|
650
|
+
const receipt = deliveryReceipt(ctx, event, now());
|
|
651
|
+
await bridge.acknowledgeDelivery(ctx.monitorId, receipt);
|
|
652
|
+
receiptIds.push(receipt.object_id);
|
|
653
|
+
if (event.payload.actionability !== 'advisory') {
|
|
654
|
+
state = reduceMonitor(spec, state, { kind: 'acknowledge', sequence: event.payload.sequence }).state;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return {
|
|
658
|
+
state,
|
|
659
|
+
receiptIds,
|
|
660
|
+
continuationPending: outstanding.length > recordLimit,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
async function persistRecords(ctx, bridge, spec, {
|
|
665
|
+
initialState,
|
|
666
|
+
records,
|
|
667
|
+
snapshot,
|
|
668
|
+
subjectRevision,
|
|
669
|
+
timestamp,
|
|
670
|
+
batchRecords = records,
|
|
671
|
+
batchOffset = 0,
|
|
672
|
+
pendingBatch = null,
|
|
673
|
+
recordsAreBounded = false,
|
|
674
|
+
batchPlan = null,
|
|
675
|
+
recordLimit = MAX_EVENT_DELTAS,
|
|
676
|
+
}) {
|
|
677
|
+
const batchEvidence = batchPlan ? null : pendingBatch || (batchRecords.length > 1
|
|
678
|
+
? encodePendingBatch(ctx, subjectRevision, batchRecords, snapshot)
|
|
679
|
+
: null);
|
|
680
|
+
const entries = records.map((record, index) => {
|
|
681
|
+
const checkpointComplete = batchOffset + index === batchRecords.length - 1;
|
|
682
|
+
return {
|
|
683
|
+
record,
|
|
684
|
+
snapshot: checkpointComplete ? snapshot : null,
|
|
685
|
+
checkpointComplete,
|
|
686
|
+
pendingBatch: checkpointComplete ? null : batchEvidence,
|
|
687
|
+
recordIsBounded: recordsAreBounded,
|
|
688
|
+
batchPlan: typeof batchPlan === 'function' ? batchPlan(batchOffset + index) : batchPlan,
|
|
689
|
+
actual: true,
|
|
690
|
+
};
|
|
691
|
+
});
|
|
692
|
+
return persistEntries(ctx, bridge, spec, {
|
|
693
|
+
initialState,
|
|
694
|
+
entries,
|
|
695
|
+
subjectRevision,
|
|
696
|
+
timestamp,
|
|
697
|
+
recordLimit,
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
async function persistEntries(ctx, bridge, spec, {
|
|
702
|
+
initialState,
|
|
703
|
+
entries,
|
|
704
|
+
subjectRevision,
|
|
705
|
+
timestamp,
|
|
706
|
+
recordLimit = MAX_EVENT_DELTAS,
|
|
707
|
+
}) {
|
|
708
|
+
let validatedState = initialState;
|
|
709
|
+
const prepared = [];
|
|
710
|
+
for (const entry of entries) {
|
|
711
|
+
const event = monitorEvent(
|
|
712
|
+
ctx,
|
|
713
|
+
entry.record,
|
|
714
|
+
entry.snapshot,
|
|
715
|
+
subjectRevision,
|
|
716
|
+
entry.checkpointComplete,
|
|
717
|
+
entry.pendingBatch,
|
|
718
|
+
entry.recordIsBounded === true,
|
|
719
|
+
entry.batchPlan,
|
|
720
|
+
);
|
|
721
|
+
const transition = reduceMonitor(spec, validatedState, { kind: 'observation', event });
|
|
722
|
+
const receipt = deliveryReceipt(ctx, event, timestamp);
|
|
723
|
+
const nextState = event.payload.actionability === 'advisory'
|
|
724
|
+
? transition.state
|
|
725
|
+
: reduceMonitor(spec, transition.state, { kind: 'acknowledge', sequence: entry.record.seq }).state;
|
|
726
|
+
prepared.push({ entry, event, receipt, nextState });
|
|
727
|
+
validatedState = nextState;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
let state = initialState;
|
|
731
|
+
const receiptIds = [];
|
|
732
|
+
const boundedPrepared = prepared.slice(0, recordLimit);
|
|
733
|
+
for (const { event, receipt, nextState } of boundedPrepared) {
|
|
734
|
+
await bridge.persistEvent(event);
|
|
735
|
+
await bridge.acknowledgeDelivery(ctx.monitorId, receipt);
|
|
736
|
+
receiptIds.push(receipt.object_id);
|
|
737
|
+
state = nextState;
|
|
738
|
+
}
|
|
739
|
+
return {
|
|
740
|
+
state,
|
|
741
|
+
receiptIds,
|
|
742
|
+
records: boundedPrepared.filter(item => item.entry.actual === true).map(item => item.entry.record),
|
|
743
|
+
checkpointComplete: boundedPrepared.at(-1)?.entry.checkpointComplete === true,
|
|
744
|
+
continuationPending: boundedPrepared.length < entries.length,
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
async function persistSegmentedRecords(ctx, bridge, spec, {
|
|
749
|
+
initialState,
|
|
750
|
+
records,
|
|
751
|
+
snapshot,
|
|
752
|
+
subjectRevision,
|
|
753
|
+
timestamp,
|
|
754
|
+
recordLimit = MAX_EVENT_DELTAS,
|
|
755
|
+
}) {
|
|
756
|
+
let segmentCount = 2;
|
|
757
|
+
let shiftedRecords;
|
|
758
|
+
let batches;
|
|
759
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
760
|
+
shiftedRecords = records.map(record => ({ ...record, seq: record.seq + segmentCount + 1 }));
|
|
761
|
+
batches = splitPendingBatches(ctx, subjectRevision, shiftedRecords, snapshot);
|
|
762
|
+
if (batches.length === segmentCount) break;
|
|
763
|
+
segmentCount = batches.length;
|
|
764
|
+
}
|
|
765
|
+
if (!batches || batches.length !== segmentCount || segmentCount < 2) {
|
|
766
|
+
throw new FlowMonitorError('INVALID_OBSERVATION', 'Pending monitor transition plan did not converge');
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const planId = computeContentHash({
|
|
770
|
+
monitor_id: ctx.monitorId,
|
|
771
|
+
subject_revision: subjectRevision,
|
|
772
|
+
segments: batches.map(batch => batch.encoded),
|
|
773
|
+
});
|
|
774
|
+
const startSequence = records[0].seq;
|
|
775
|
+
const planCandidates = [
|
|
776
|
+
...batches.map((_batch, index) => makeEvent(
|
|
777
|
+
T.SNAPSHOT_CHECKPOINT,
|
|
778
|
+
`batch-plan:${planId}:${index}`,
|
|
779
|
+
{},
|
|
780
|
+
)),
|
|
781
|
+
makeEvent(T.SNAPSHOT_CHECKPOINT, `batch-plan:${planId}:commit`, {}),
|
|
782
|
+
];
|
|
783
|
+
const planRecords = finalizeRecords(planCandidates, {
|
|
784
|
+
baseSeq: startSequence - 1,
|
|
785
|
+
ts: timestamp,
|
|
786
|
+
snapshot,
|
|
787
|
+
});
|
|
788
|
+
const entries = [
|
|
789
|
+
...batches.map((batch, index) => ({
|
|
790
|
+
record: planRecords[index],
|
|
791
|
+
snapshot: null,
|
|
792
|
+
checkpointComplete: false,
|
|
793
|
+
pendingBatch: batch.encoded,
|
|
794
|
+
batchPlan: batchPlanReference(planId, startSequence, segmentCount, { index }),
|
|
795
|
+
actual: false,
|
|
796
|
+
})),
|
|
797
|
+
{
|
|
798
|
+
record: planRecords.at(-1),
|
|
799
|
+
snapshot: null,
|
|
800
|
+
checkpointComplete: false,
|
|
801
|
+
pendingBatch: null,
|
|
802
|
+
batchPlan: batchPlanReference(planId, startSequence, segmentCount, { committed: true }),
|
|
803
|
+
actual: false,
|
|
804
|
+
},
|
|
805
|
+
...shiftedRecords.map((record, index) => ({
|
|
806
|
+
record,
|
|
807
|
+
snapshot: index === shiftedRecords.length - 1 ? snapshot : null,
|
|
808
|
+
checkpointComplete: index === shiftedRecords.length - 1,
|
|
809
|
+
pendingBatch: null,
|
|
810
|
+
batchPlan: batchPlanReference(planId, startSequence, segmentCount, {
|
|
811
|
+
segment_index: batchPlanSegmentIndex(batches, index),
|
|
812
|
+
}),
|
|
813
|
+
actual: true,
|
|
814
|
+
})),
|
|
815
|
+
];
|
|
816
|
+
return persistEntries(ctx, bridge, spec, {
|
|
817
|
+
initialState,
|
|
818
|
+
entries,
|
|
819
|
+
subjectRevision,
|
|
820
|
+
timestamp,
|
|
821
|
+
recordLimit,
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
function planRecordKey(planId, index) {
|
|
826
|
+
return `batch-plan:${planId}:${index}`;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
async function readPlannedEvent(ctx, durableEvents, sequence, key) {
|
|
830
|
+
const expectedId = monitorEventId(ctx.monitorId, sequence, T.SNAPSHOT_CHECKPOINT, key);
|
|
831
|
+
const retained = durableEvents.find(event => event.payload.sequence === sequence);
|
|
832
|
+
if (retained) return retained.payload.event_id === expectedId ? retained : null;
|
|
833
|
+
if (typeof ctx.store.getEvent !== 'function') return null;
|
|
834
|
+
let row;
|
|
835
|
+
try {
|
|
836
|
+
row = await ctx.store.getEvent(expectedId);
|
|
837
|
+
} catch (error) {
|
|
838
|
+
throw new FlowMonitorError('PROVIDER_UNAVAILABLE', 'Durable monitor transition plan is unavailable', { cause: error });
|
|
839
|
+
}
|
|
840
|
+
if (!row) return null;
|
|
841
|
+
const event = parseEventRow(row, ctx.monitorId);
|
|
842
|
+
return event.payload.event_id === expectedId && event.payload.sequence === sequence ? event : null;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
async function readSegmentedRecovery(ctx, durableEvents, pendingEvents, subjectRevision) {
|
|
846
|
+
const references = [];
|
|
847
|
+
for (let index = pendingEvents.length - 1; index >= 0; index -= 1) {
|
|
848
|
+
const reference = pendingEvents[index].payload.bounded_payload.batch_plan;
|
|
849
|
+
if (validBatchPlanReference(reference)
|
|
850
|
+
&& !references.some(item => item.id === reference.id)) {
|
|
851
|
+
references.push(reference);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
for (const reference of references) {
|
|
856
|
+
const markerSequence = reference.start_sequence + reference.segment_count;
|
|
857
|
+
const referenceEvents = pendingEvents.filter(event => (
|
|
858
|
+
event.payload.bounded_payload.batch_plan?.id === reference.id
|
|
859
|
+
));
|
|
860
|
+
const committedHint = referenceEvents.some(event => (
|
|
861
|
+
event.payload.bounded_payload.batch_plan?.committed === true
|
|
862
|
+
|| event.payload.sequence > markerSequence
|
|
863
|
+
));
|
|
864
|
+
const marker = await readPlannedEvent(
|
|
865
|
+
ctx,
|
|
866
|
+
durableEvents,
|
|
867
|
+
markerSequence,
|
|
868
|
+
planRecordKey(reference.id, 'commit'),
|
|
869
|
+
);
|
|
870
|
+
if (!marker) {
|
|
871
|
+
if (committedHint) {
|
|
872
|
+
throw new FlowMonitorError('MONITOR_HISTORY_INCOMPLETE', 'Durable monitor transition plan marker is missing');
|
|
873
|
+
}
|
|
874
|
+
const segmentIndexes = referenceEvents
|
|
875
|
+
.map(event => event.payload.bounded_payload.batch_plan?.index)
|
|
876
|
+
.filter(Number.isSafeInteger);
|
|
877
|
+
if (segmentIndexes.length !== reference.segment_count
|
|
878
|
+
|| new Set(segmentIndexes).size !== reference.segment_count
|
|
879
|
+
|| !segmentIndexes.every(index => index >= 0 && index < reference.segment_count)) {
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
if (marker) {
|
|
884
|
+
const markerReference = marker.payload.bounded_payload.batch_plan;
|
|
885
|
+
const markerRecord = marker.payload.bounded_payload.record;
|
|
886
|
+
const markerKey = planRecordKey(reference.id, 'commit');
|
|
887
|
+
if (!validBatchPlanReference(markerReference)
|
|
888
|
+
|| markerReference.id !== reference.id
|
|
889
|
+
|| markerReference.start_sequence !== reference.start_sequence
|
|
890
|
+
|| markerReference.segment_count !== reference.segment_count
|
|
891
|
+
|| markerReference.committed !== true
|
|
892
|
+
|| marker.payload.event_id !== monitorEventId(
|
|
893
|
+
ctx.monitorId,
|
|
894
|
+
markerSequence,
|
|
895
|
+
T.SNAPSHOT_CHECKPOINT,
|
|
896
|
+
markerKey,
|
|
897
|
+
)
|
|
898
|
+
|| marker.payload.sequence !== markerSequence
|
|
899
|
+
|| marker.payload.subject_revision !== subjectRevision
|
|
900
|
+
|| marker.payload.type !== T.SNAPSHOT_CHECKPOINT
|
|
901
|
+
|| marker.payload.actionability !== 'advisory'
|
|
902
|
+
|| markerRecord?.key !== boundIdentity(markerKey)
|
|
903
|
+
|| markerRecord?.seq !== markerSequence
|
|
904
|
+
|| markerRecord?.type !== T.SNAPSHOT_CHECKPOINT
|
|
905
|
+
|| marker.payload.observed_at !== markerRecord?.ts
|
|
906
|
+
|| marker.payload.bounded_payload.snapshot !== null
|
|
907
|
+
|| marker.payload.bounded_payload.checkpoint_complete !== false
|
|
908
|
+
|| marker.payload.bounded_payload.pending_batch !== undefined) {
|
|
909
|
+
throw new FlowMonitorError('MONITOR_HISTORY_INCOMPLETE', 'Durable monitor transition plan marker is inconsistent');
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const batches = [];
|
|
914
|
+
for (let index = 0; index < reference.segment_count; index += 1) {
|
|
915
|
+
const segmentSequence = reference.start_sequence + index;
|
|
916
|
+
const segmentKey = planRecordKey(reference.id, index);
|
|
917
|
+
const segment = await readPlannedEvent(
|
|
918
|
+
ctx,
|
|
919
|
+
durableEvents,
|
|
920
|
+
segmentSequence,
|
|
921
|
+
segmentKey,
|
|
922
|
+
);
|
|
923
|
+
const segmentReference = segment?.payload.bounded_payload.batch_plan;
|
|
924
|
+
const pendingBatch = segment?.payload.bounded_payload.pending_batch;
|
|
925
|
+
const segmentRecord = segment?.payload.bounded_payload.record;
|
|
926
|
+
if (!segment
|
|
927
|
+
|| !validBatchPlanReference(segmentReference)
|
|
928
|
+
|| segmentReference.id !== reference.id
|
|
929
|
+
|| segmentReference.start_sequence !== reference.start_sequence
|
|
930
|
+
|| segmentReference.segment_count !== reference.segment_count
|
|
931
|
+
|| segmentReference.index !== index
|
|
932
|
+
|| segmentReference.committed !== undefined
|
|
933
|
+
|| segmentReference.segment_index !== undefined
|
|
934
|
+
|| segment.payload.event_id !== monitorEventId(
|
|
935
|
+
ctx.monitorId,
|
|
936
|
+
segmentSequence,
|
|
937
|
+
T.SNAPSHOT_CHECKPOINT,
|
|
938
|
+
segmentKey,
|
|
939
|
+
)
|
|
940
|
+
|| segment.payload.sequence !== segmentSequence
|
|
941
|
+
|| segment.payload.subject_revision !== subjectRevision
|
|
942
|
+
|| segment.payload.type !== T.SNAPSHOT_CHECKPOINT
|
|
943
|
+
|| segment.payload.actionability !== 'advisory'
|
|
944
|
+
|| segmentRecord?.key !== boundIdentity(segmentKey)
|
|
945
|
+
|| segmentRecord?.seq !== segmentSequence
|
|
946
|
+
|| segmentRecord?.type !== T.SNAPSHOT_CHECKPOINT
|
|
947
|
+
|| segment.payload.observed_at !== segmentRecord?.ts
|
|
948
|
+
|| segment.payload.bounded_payload.snapshot !== null
|
|
949
|
+
|| segment.payload.bounded_payload.checkpoint_complete !== false
|
|
950
|
+
|| typeof pendingBatch !== 'string') {
|
|
951
|
+
throw new FlowMonitorError('MONITOR_HISTORY_INCOMPLETE', 'Durable monitor transition plan segment is inconsistent');
|
|
952
|
+
}
|
|
953
|
+
batches.push(decodePendingBatch(pendingBatch, ctx, subjectRevision));
|
|
954
|
+
}
|
|
955
|
+
const planId = computeContentHash({
|
|
956
|
+
monitor_id: ctx.monitorId,
|
|
957
|
+
subject_revision: subjectRevision,
|
|
958
|
+
segments: batches.map(batch => batch.encoded),
|
|
959
|
+
});
|
|
960
|
+
if (planId !== reference.id
|
|
961
|
+
|| batches.slice(0, -1).some(batch => batch.snapshot !== null)
|
|
962
|
+
|| batches.at(-1).snapshot === null) {
|
|
963
|
+
throw new FlowMonitorError('MONITOR_HISTORY_INCOMPLETE', 'Durable monitor transition plan identity is inconsistent');
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
const records = batches.flatMap(batch => batch.records);
|
|
967
|
+
const actualStart = markerSequence + 1;
|
|
968
|
+
if (records.some((record, index) => record.seq !== actualStart + index)) {
|
|
969
|
+
throw new FlowMonitorError('MONITOR_HISTORY_INCOMPLETE', 'Durable monitor transition plan sequence is inconsistent');
|
|
970
|
+
}
|
|
971
|
+
const actualEvents = pendingEvents.filter(event => event.payload.sequence >= actualStart);
|
|
972
|
+
const retainedOffset = actualEvents.length > 0
|
|
973
|
+
? actualEvents[0].payload.sequence - actualStart
|
|
974
|
+
: 0;
|
|
975
|
+
const remainingOffset = retainedOffset + actualEvents.length;
|
|
976
|
+
if (!Number.isSafeInteger(retainedOffset) || retainedOffset < 0
|
|
977
|
+
|| remainingOffset > records.length) {
|
|
978
|
+
throw new FlowMonitorError('MONITOR_HISTORY_INCOMPLETE', 'Durable monitor transition plan has no remaining checkpoint');
|
|
979
|
+
}
|
|
980
|
+
for (const [index, event] of actualEvents.entries()) {
|
|
981
|
+
const eventReference = event.payload.bounded_payload.batch_plan;
|
|
982
|
+
const recordOffset = retainedOffset + index;
|
|
983
|
+
const expectedRecord = records[recordOffset];
|
|
984
|
+
const checkpointComplete = recordOffset === records.length - 1;
|
|
985
|
+
const expectedActionability = expectedRecord.type === T.SNAPSHOT_CHECKPOINT
|
|
986
|
+
? 'advisory'
|
|
987
|
+
: 'action_required';
|
|
988
|
+
if (!validBatchPlanReference(eventReference)
|
|
989
|
+
|| eventReference.id !== reference.id
|
|
990
|
+
|| eventReference.segment_index !== batchPlanSegmentIndex(batches, recordOffset)
|
|
991
|
+
|| event.payload.sequence !== actualStart + recordOffset
|
|
992
|
+
|| event.payload.event_id !== monitorEventId(
|
|
993
|
+
ctx.monitorId,
|
|
994
|
+
expectedRecord.seq,
|
|
995
|
+
expectedRecord.type,
|
|
996
|
+
expectedRecord.key,
|
|
997
|
+
)
|
|
998
|
+
|| event.payload.type !== expectedRecord.type
|
|
999
|
+
|| event.payload.actionability !== expectedActionability
|
|
1000
|
+
|| event.payload.bounded_payload.checkpoint_complete !== checkpointComplete
|
|
1001
|
+
|| event.payload.bounded_payload.pending_batch !== undefined
|
|
1002
|
+
|| computeContentHash({ snapshot: event.payload.bounded_payload.snapshot ?? null })
|
|
1003
|
+
!== computeContentHash({ snapshot: checkpointComplete ? batches.at(-1).snapshot : null })
|
|
1004
|
+
|| computeContentHash(event.payload.bounded_payload.record) !== computeContentHash(expectedRecord)) {
|
|
1005
|
+
throw new FlowMonitorError('MONITOR_HISTORY_INCOMPLETE', 'Pending monitor transition identity is inconsistent');
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
return {
|
|
1009
|
+
records,
|
|
1010
|
+
remainingOffset,
|
|
1011
|
+
snapshot: batches.at(-1).snapshot,
|
|
1012
|
+
reference: batchPlanReference(
|
|
1013
|
+
reference.id,
|
|
1014
|
+
reference.start_sequence,
|
|
1015
|
+
reference.segment_count,
|
|
1016
|
+
),
|
|
1017
|
+
batches,
|
|
1018
|
+
needsCommit: !marker,
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
return null;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function validOrphanedPrecommitPlan(ctx, pendingEvents, subjectRevision) {
|
|
1025
|
+
if (pendingEvents.length === 0) return false;
|
|
1026
|
+
let offset = 0;
|
|
1027
|
+
while (offset < pendingEvents.length) {
|
|
1028
|
+
const reference = pendingEvents[offset].payload.bounded_payload.batch_plan;
|
|
1029
|
+
const startIndex = reference?.index;
|
|
1030
|
+
if (!validBatchPlanReference(reference)
|
|
1031
|
+
|| !Number.isSafeInteger(startIndex) || startIndex < 0
|
|
1032
|
+
|| startIndex >= reference.segment_count
|
|
1033
|
+
|| reference.start_sequence + startIndex !== pendingEvents[offset].payload.sequence) return false;
|
|
1034
|
+
const batches = [];
|
|
1035
|
+
while (offset < pendingEvents.length) {
|
|
1036
|
+
const event = pendingEvents[offset];
|
|
1037
|
+
const candidate = event.payload.bounded_payload.batch_plan;
|
|
1038
|
+
if (candidate?.id !== reference.id) break;
|
|
1039
|
+
const index = startIndex + batches.length;
|
|
1040
|
+
const pendingBatch = event.payload.bounded_payload.pending_batch;
|
|
1041
|
+
const record = event.payload.bounded_payload.record;
|
|
1042
|
+
const key = planRecordKey(reference.id, index);
|
|
1043
|
+
if (event.payload.actionability !== 'advisory'
|
|
1044
|
+
|| !validBatchPlanReference(candidate)
|
|
1045
|
+
|| candidate.start_sequence !== reference.start_sequence
|
|
1046
|
+
|| candidate.segment_count !== reference.segment_count
|
|
1047
|
+
|| candidate.index !== index
|
|
1048
|
+
|| index >= reference.segment_count
|
|
1049
|
+
|| candidate.committed !== undefined
|
|
1050
|
+
|| candidate.segment_index !== undefined
|
|
1051
|
+
|| event.payload.sequence !== reference.start_sequence + index
|
|
1052
|
+
|| event.payload.event_id !== monitorEventId(
|
|
1053
|
+
ctx.monitorId,
|
|
1054
|
+
event.payload.sequence,
|
|
1055
|
+
T.SNAPSHOT_CHECKPOINT,
|
|
1056
|
+
key,
|
|
1057
|
+
)
|
|
1058
|
+
|| event.payload.subject_revision !== subjectRevision
|
|
1059
|
+
|| event.payload.type !== T.SNAPSHOT_CHECKPOINT
|
|
1060
|
+
|| event.payload.bounded_payload.snapshot !== null
|
|
1061
|
+
|| event.payload.bounded_payload.checkpoint_complete !== false
|
|
1062
|
+
|| record?.type !== T.SNAPSHOT_CHECKPOINT
|
|
1063
|
+
|| record?.key !== boundIdentity(key)
|
|
1064
|
+
|| record?.seq !== event.payload.sequence
|
|
1065
|
+
|| event.payload.observed_at !== record?.ts
|
|
1066
|
+
|| typeof pendingBatch !== 'string') return false;
|
|
1067
|
+
try {
|
|
1068
|
+
batches.push(decodePendingBatch(pendingBatch, ctx, subjectRevision));
|
|
1069
|
+
} catch {
|
|
1070
|
+
return false;
|
|
1071
|
+
}
|
|
1072
|
+
offset += 1;
|
|
1073
|
+
}
|
|
1074
|
+
const complete = startIndex + batches.length === reference.segment_count;
|
|
1075
|
+
if (!complete) {
|
|
1076
|
+
if (batches.some(batch => batch.snapshot !== null)) return false;
|
|
1077
|
+
if (startIndex > 0 && offset === pendingEvents.length) return false;
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
if (batches.slice(0, -1).some(batch => batch.snapshot !== null)
|
|
1081
|
+
|| batches.at(-1).snapshot === null) return false;
|
|
1082
|
+
if (startIndex > 0) {
|
|
1083
|
+
if (offset === pendingEvents.length) return false;
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
1086
|
+
const planId = computeContentHash({
|
|
1087
|
+
monitor_id: ctx.monitorId,
|
|
1088
|
+
subject_revision: subjectRevision,
|
|
1089
|
+
segments: batches.map(batch => batch.encoded),
|
|
1090
|
+
});
|
|
1091
|
+
if (planId !== reference.id) return false;
|
|
1092
|
+
}
|
|
1093
|
+
return true;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
function remainingSegmentedRecords(segmented) {
|
|
1097
|
+
const remaining = segmented.records.slice(segmented.remainingOffset);
|
|
1098
|
+
if (remaining.length === 0) {
|
|
1099
|
+
throw new FlowMonitorError(
|
|
1100
|
+
'MONITOR_HISTORY_INCOMPLETE',
|
|
1101
|
+
'Pending monitor transition has no remaining records',
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
return remaining;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
function segmentedContinuationEntries(segmented) {
|
|
1108
|
+
const entries = [];
|
|
1109
|
+
if (segmented.needsCommit) {
|
|
1110
|
+
const markerSequence = segmented.reference.start_sequence + segmented.reference.segment_count;
|
|
1111
|
+
const markerRecord = finalizeRecords([
|
|
1112
|
+
makeEvent(T.SNAPSHOT_CHECKPOINT, planRecordKey(segmented.reference.id, 'commit'), {}),
|
|
1113
|
+
], {
|
|
1114
|
+
baseSeq: markerSequence - 1,
|
|
1115
|
+
ts: segmented.records[0].ts,
|
|
1116
|
+
snapshot: segmented.snapshot,
|
|
1117
|
+
})[0];
|
|
1118
|
+
entries.push({
|
|
1119
|
+
record: markerRecord,
|
|
1120
|
+
snapshot: null,
|
|
1121
|
+
checkpointComplete: false,
|
|
1122
|
+
pendingBatch: null,
|
|
1123
|
+
batchPlan: { ...segmented.reference, committed: true },
|
|
1124
|
+
actual: false,
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
for (let offset = segmented.remainingOffset; offset < segmented.records.length; offset += 1) {
|
|
1128
|
+
entries.push({
|
|
1129
|
+
record: segmented.records[offset],
|
|
1130
|
+
snapshot: offset === segmented.records.length - 1 ? segmented.snapshot : null,
|
|
1131
|
+
checkpointComplete: offset === segmented.records.length - 1,
|
|
1132
|
+
pendingBatch: null,
|
|
1133
|
+
batchPlan: {
|
|
1134
|
+
...segmented.reference,
|
|
1135
|
+
segment_index: batchPlanSegmentIndex(segmented.batches, offset),
|
|
1136
|
+
},
|
|
1137
|
+
actual: true,
|
|
1138
|
+
recordIsBounded: true,
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
return entries;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
async function finalizePass(ctx, bridge, spec, {
|
|
1145
|
+
state,
|
|
1146
|
+
snapshot,
|
|
1147
|
+
receiptIds,
|
|
1148
|
+
createdAt,
|
|
1149
|
+
records,
|
|
1150
|
+
continuationPending = false,
|
|
1151
|
+
}) {
|
|
1152
|
+
const terminalDeferred = !continuationPending
|
|
1153
|
+
&& receiptIds.length >= MAX_EVENT_DELTAS
|
|
1154
|
+
&& terminalPrState(snapshot) !== null;
|
|
1155
|
+
const hasContinuation = continuationPending || terminalDeferred;
|
|
1156
|
+
const terminalReceiptId = hasContinuation
|
|
1157
|
+
? null
|
|
1158
|
+
: await recordTerminalHandoff(ctx, bridge, spec, state, snapshot, createdAt);
|
|
1159
|
+
if (terminalReceiptId) receiptIds.push(terminalReceiptId);
|
|
1160
|
+
return {
|
|
1161
|
+
events: records,
|
|
1162
|
+
changed: records.length > 0,
|
|
1163
|
+
authority: 'memory',
|
|
1164
|
+
receiptIds,
|
|
1165
|
+
...(hasContinuation ? { continuationPending: true } : {}),
|
|
1166
|
+
...(terminalReceiptId ? { terminalReceiptId } : {}),
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
async function runFlowMonitorPass(ctx, reopenDepth = 0) {
|
|
1171
|
+
const now = ctx.now || defaultNow;
|
|
1172
|
+
const subjectRevision = ctx.subjectRevision || ctx.subjectId;
|
|
1173
|
+
const spec = monitorSpec(ctx, subjectRevision);
|
|
1174
|
+
const { events: durableEvents, state: replayedState } = await readAuthority(ctx, spec);
|
|
1175
|
+
let state = replayedState;
|
|
1176
|
+
|
|
1177
|
+
const deliveryState = await readDeliveryAuthority(ctx);
|
|
1178
|
+
const replay = terminalReplay(deliveryState, ctx.monitorId);
|
|
1179
|
+
if (replay) {
|
|
1180
|
+
let gathered; let current;
|
|
1181
|
+
try {
|
|
1182
|
+
gathered = await ctx.gather();
|
|
1183
|
+
current = compactSnapshot(gathered);
|
|
1184
|
+
} catch { return replay; }
|
|
1185
|
+
const lifecycle = String(current?.prState || current?.state || '').toUpperCase();
|
|
1186
|
+
const monitorId = reopenedMonitorId(ctx.monitorId, replay.terminalReceiptId);
|
|
1187
|
+
const reopenedContext = { ...ctx, monitorId };
|
|
1188
|
+
const reopenedSpec = monitorSpec(reopenedContext, subjectRevision);
|
|
1189
|
+
const { events: reopenedHistory } = await readAuthority(reopenedContext, reopenedSpec);
|
|
1190
|
+
if (lifecycle !== 'OPEN' && reopenedHistory.length === 0) return replay;
|
|
1191
|
+
if (reopenDepth >= MAX_REOPEN_DEPTH) {
|
|
1192
|
+
throw new FlowMonitorError('MONITOR_HISTORY_INCOMPLETE', 'Reopened monitor lifecycle exceeds its restart bound');
|
|
1193
|
+
}
|
|
1194
|
+
return runFlowMonitorPass({
|
|
1195
|
+
...ctx,
|
|
1196
|
+
monitorId,
|
|
1197
|
+
gather: async () => gathered,
|
|
1198
|
+
}, reopenDepth + 1);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
let checkpointIndex = -1;
|
|
1202
|
+
for (let index = durableEvents.length - 1; index >= 0; index -= 1) {
|
|
1203
|
+
if (durableEvents[index].payload.bounded_payload.checkpoint_complete !== false) {
|
|
1204
|
+
checkpointIndex = index;
|
|
1205
|
+
break;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
const previousSnapshot = checkpointIndex >= 0
|
|
1209
|
+
? durableEvents[checkpointIndex].payload.bounded_payload.snapshot ?? null
|
|
1210
|
+
: null;
|
|
1211
|
+
const pendingEvents = durableEvents.slice(checkpointIndex + 1);
|
|
1212
|
+
const deliveryCursor = deliveryState.cursors.find(item => item.target === DELIVERY_TARGET)?.sequence ?? 0;
|
|
1213
|
+
const unacknowledgedPlanEvents = durableEvents.filter(event => (
|
|
1214
|
+
event.payload.sequence > deliveryCursor && event.payload.bounded_payload.batch_plan
|
|
1215
|
+
));
|
|
1216
|
+
const recoveryEvents = pendingEvents.some(event => event.payload.bounded_payload.batch_plan)
|
|
1217
|
+
? pendingEvents
|
|
1218
|
+
: unacknowledgedPlanEvents;
|
|
1219
|
+
const segmented = recoveryEvents.length > 0
|
|
1220
|
+
? await readSegmentedRecovery(ctx, durableEvents, recoveryEvents, subjectRevision)
|
|
1221
|
+
: null;
|
|
1222
|
+
const hasBatchPlan = recoveryEvents.some(event => event.payload.bounded_payload.batch_plan);
|
|
1223
|
+
const orphanedPlan = !segmented && hasBatchPlan
|
|
1224
|
+
&& recoveryEvents === pendingEvents
|
|
1225
|
+
&& validOrphanedPrecommitPlan(ctx, pendingEvents, subjectRevision);
|
|
1226
|
+
if (!segmented && hasBatchPlan && !orphanedPlan) {
|
|
1227
|
+
throw new FlowMonitorError('MONITOR_HISTORY_INCOMPLETE', 'Durable monitor transition plan is inconsistent');
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
const bridge = createMonitorDurabilityBridge({
|
|
1231
|
+
store: bridgeStore(ctx.store),
|
|
1232
|
+
deliveryTargets: [DELIVERY_TARGET],
|
|
1233
|
+
deliver: async (event) => event.payload.actionability === 'advisory'
|
|
1234
|
+
? undefined
|
|
1235
|
+
: ctx.deliverLegacy(event.payload.bounded_payload.record, event.payload.event_id),
|
|
1236
|
+
});
|
|
1237
|
+
const replayed = await redeliverOutstanding(ctx, bridge, spec, state, durableEvents, deliveryState, now);
|
|
1238
|
+
state = replayed.state;
|
|
1239
|
+
const receiptIds = replayed.receiptIds;
|
|
1240
|
+
let remainingBudget = MAX_EVENT_DELTAS - receiptIds.length;
|
|
1241
|
+
if (replayed.continuationPending || remainingBudget === 0) {
|
|
1242
|
+
return finalizePass(ctx, bridge, spec, {
|
|
1243
|
+
state,
|
|
1244
|
+
snapshot: previousSnapshot,
|
|
1245
|
+
receiptIds,
|
|
1246
|
+
createdAt: now(),
|
|
1247
|
+
records: [],
|
|
1248
|
+
continuationPending: true,
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
if (pendingEvents.length > 0) {
|
|
1252
|
+
if (segmented) {
|
|
1253
|
+
const remaining = remainingSegmentedRecords(segmented);
|
|
1254
|
+
const timestamp = remaining[0].ts;
|
|
1255
|
+
const persisted = segmented.needsCommit
|
|
1256
|
+
? await persistEntries(ctx, bridge, spec, {
|
|
1257
|
+
initialState: state,
|
|
1258
|
+
entries: segmentedContinuationEntries(segmented),
|
|
1259
|
+
subjectRevision,
|
|
1260
|
+
timestamp,
|
|
1261
|
+
recordLimit: remainingBudget,
|
|
1262
|
+
})
|
|
1263
|
+
: await persistRecords(ctx, bridge, spec, {
|
|
1264
|
+
initialState: state,
|
|
1265
|
+
records: remaining,
|
|
1266
|
+
snapshot: segmented.snapshot,
|
|
1267
|
+
subjectRevision,
|
|
1268
|
+
timestamp,
|
|
1269
|
+
batchRecords: segmented.records,
|
|
1270
|
+
batchOffset: segmented.remainingOffset,
|
|
1271
|
+
recordsAreBounded: true,
|
|
1272
|
+
batchPlan: offset => ({
|
|
1273
|
+
...segmented.reference,
|
|
1274
|
+
segment_index: batchPlanSegmentIndex(segmented.batches, offset),
|
|
1275
|
+
}),
|
|
1276
|
+
recordLimit: remainingBudget,
|
|
1277
|
+
});
|
|
1278
|
+
receiptIds.push(...persisted.receiptIds);
|
|
1279
|
+
return finalizePass(ctx, bridge, spec, {
|
|
1280
|
+
state: persisted.state,
|
|
1281
|
+
snapshot: persisted.checkpointComplete ? segmented.snapshot : null,
|
|
1282
|
+
receiptIds,
|
|
1283
|
+
createdAt: timestamp,
|
|
1284
|
+
records: persisted.records,
|
|
1285
|
+
continuationPending: persisted.checkpointComplete !== true,
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
if (orphanedPlan) {
|
|
1289
|
+
// A crash before the commit marker leaves only advisory preparation events.
|
|
1290
|
+
// No provider transition was accepted, so a fresh gather may safely supersede it.
|
|
1291
|
+
} else {
|
|
1292
|
+
const pendingValue = pendingEvents.at(-1).payload.bounded_payload.pending_batch;
|
|
1293
|
+
const batch = decodePendingBatch(pendingValue, ctx, subjectRevision);
|
|
1294
|
+
const retainedOffset = pendingEvents[0].payload.sequence - batch.records[0].seq;
|
|
1295
|
+
const remainingOffset = retainedOffset + pendingEvents.length;
|
|
1296
|
+
if (!Number.isSafeInteger(retainedOffset) || retainedOffset < 0
|
|
1297
|
+
|| remainingOffset >= batch.records.length) {
|
|
1298
|
+
throw new FlowMonitorError('MONITOR_HISTORY_INCOMPLETE', 'Pending monitor transition batch has no remaining checkpoint');
|
|
1299
|
+
}
|
|
1300
|
+
for (const [index, pending] of pendingEvents.entries()) {
|
|
1301
|
+
if (pending.payload.bounded_payload.pending_batch !== batch.encoded
|
|
1302
|
+
|| computeContentHash(pending.payload.bounded_payload.record) !== computeContentHash(batch.records[retainedOffset + index])) {
|
|
1303
|
+
throw new FlowMonitorError('MONITOR_HISTORY_INCOMPLETE', 'Pending monitor transition identity is inconsistent');
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
const remaining = batch.records.slice(remainingOffset);
|
|
1307
|
+
const timestamp = remaining[0].ts;
|
|
1308
|
+
const persisted = await persistRecords(ctx, bridge, spec, {
|
|
1309
|
+
initialState: state,
|
|
1310
|
+
records: remaining,
|
|
1311
|
+
snapshot: batch.snapshot,
|
|
1312
|
+
subjectRevision,
|
|
1313
|
+
timestamp,
|
|
1314
|
+
batchRecords: batch.records,
|
|
1315
|
+
batchOffset: remainingOffset,
|
|
1316
|
+
pendingBatch: batch.encoded,
|
|
1317
|
+
recordsAreBounded: true,
|
|
1318
|
+
recordLimit: remainingBudget,
|
|
1319
|
+
});
|
|
1320
|
+
receiptIds.push(...persisted.receiptIds);
|
|
1321
|
+
return finalizePass(ctx, bridge, spec, {
|
|
1322
|
+
state: persisted.state,
|
|
1323
|
+
snapshot: persisted.checkpointComplete ? batch.snapshot : null,
|
|
1324
|
+
receiptIds,
|
|
1325
|
+
createdAt: timestamp,
|
|
1326
|
+
records: persisted.records,
|
|
1327
|
+
continuationPending: persisted.checkpointComplete !== true,
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
if (receiptIds.length > 0) {
|
|
1333
|
+
return finalizePass(ctx, bridge, spec, {
|
|
1334
|
+
state,
|
|
1335
|
+
snapshot: previousSnapshot,
|
|
1336
|
+
receiptIds,
|
|
1337
|
+
createdAt: now(),
|
|
1338
|
+
records: [],
|
|
1339
|
+
continuationPending: true,
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
if (pendingEvents.length === 0 && terminalPrState(previousSnapshot) !== null) {
|
|
1344
|
+
return finalizePass(ctx, bridge, spec, {
|
|
1345
|
+
state,
|
|
1346
|
+
snapshot: previousSnapshot,
|
|
1347
|
+
receiptIds,
|
|
1348
|
+
createdAt: now(),
|
|
1349
|
+
records: [],
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
const gathered = await ctx.gather();
|
|
1354
|
+
const next = compactSnapshot(gathered);
|
|
1355
|
+
const previous = previousSnapshot == null
|
|
1356
|
+
? null
|
|
1357
|
+
: comparableSnapshot(compactSnapshot(previousSnapshot));
|
|
1358
|
+
const candidates = diffSnapshots(previous, comparableSnapshot(next, gathered));
|
|
1359
|
+
if (candidates.length === 0) {
|
|
1360
|
+
const snapshotChanged = snapshotIdentity(previousSnapshot) !== snapshotIdentity(next);
|
|
1361
|
+
if (snapshotChanged) {
|
|
1362
|
+
candidates.push(makeEvent(T.SNAPSHOT_CHECKPOINT, computeContentHash(next), {}));
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
if (candidates.length === 0) {
|
|
1366
|
+
return finalizePass(ctx, bridge, spec, {
|
|
1367
|
+
state,
|
|
1368
|
+
snapshot: next,
|
|
1369
|
+
receiptIds,
|
|
1370
|
+
createdAt: now(),
|
|
1371
|
+
records: [],
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
const timestamp = now();
|
|
1375
|
+
const records = finalizeRecords(candidates, {
|
|
1376
|
+
baseSeq: durableEvents.at(-1)?.payload.sequence ?? 0,
|
|
1377
|
+
ts: timestamp,
|
|
1378
|
+
snapshot: next,
|
|
1379
|
+
});
|
|
1380
|
+
if (typeof ctx.enrich === 'function') await ctx.enrich(records);
|
|
1381
|
+
let persisted;
|
|
1382
|
+
try {
|
|
1383
|
+
persisted = await persistRecords(ctx, bridge, spec, {
|
|
1384
|
+
initialState: state,
|
|
1385
|
+
records,
|
|
1386
|
+
snapshot: next,
|
|
1387
|
+
subjectRevision,
|
|
1388
|
+
timestamp,
|
|
1389
|
+
recordLimit: remainingBudget,
|
|
1390
|
+
});
|
|
1391
|
+
} catch (error) {
|
|
1392
|
+
if (!segmentablePendingBatchError(error)) throw error;
|
|
1393
|
+
persisted = await persistSegmentedRecords(ctx, bridge, spec, {
|
|
1394
|
+
initialState: state,
|
|
1395
|
+
records,
|
|
1396
|
+
snapshot: next,
|
|
1397
|
+
subjectRevision,
|
|
1398
|
+
timestamp,
|
|
1399
|
+
recordLimit: remainingBudget,
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
receiptIds.push(...persisted.receiptIds);
|
|
1403
|
+
return finalizePass(ctx, bridge, spec, {
|
|
1404
|
+
state: persisted.state,
|
|
1405
|
+
snapshot: persisted.checkpointComplete === false ? null : next,
|
|
1406
|
+
receiptIds,
|
|
1407
|
+
createdAt: timestamp,
|
|
1408
|
+
records: persisted.records || records,
|
|
1409
|
+
continuationPending: persisted.checkpointComplete !== true,
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
module.exports = {
|
|
1414
|
+
FlowMonitorError,
|
|
1415
|
+
privacySafeIdentity,
|
|
1416
|
+
runFlowMonitorPass,
|
|
1417
|
+
_internals: {
|
|
1418
|
+
appendPendingBatch,
|
|
1419
|
+
decodePrivateSafeBase64,
|
|
1420
|
+
encodePrivateSafeBase64,
|
|
1421
|
+
remainingSegmentedRecords,
|
|
1422
|
+
validOrphanedPrecommitPlan,
|
|
1423
|
+
},
|
|
1424
|
+
};
|