@brainervirus/workit-core 2.1.5 → 2.2.1

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.
@@ -60,6 +60,7 @@ import {
60
60
  import { diffPolicy, resolvePolicy } from "./policy-resolver";
61
61
  import { verifyStandingApproval } from "./auto-approval";
62
62
  import { TaskStore, type MetadataLock, type ProcessEvidence } from "./task-store";
63
+ import { defaultLockTimeout } from "./store-lock";
63
64
  import {
64
65
  compactTaskContext,
65
66
  exportDigest,
@@ -452,6 +453,54 @@ const refsWithinScope = (refs: Ref[], scope: Scope): boolean =>
452
453
  bindingCovers(scope, { description: "", paths: [ref.path], exclusions: [] }),
453
454
  );
454
455
 
456
+ /** Commit attempts for a call whose revisions the engine filled (see retryFilledRevisions). */
457
+ const REVISION_RETRY_ATTEMPTS = 8;
458
+ const retryPause = new Int32Array(new SharedArrayBuffer(4));
459
+ /** Which revisions the caller left for the engine to fill from its own read. */
460
+ type FilledRevisions = { task: boolean; workspace: boolean };
461
+ const filledRevisions = (request: unknown): FilledRevisions => {
462
+ const value = (typeof request === "object" && request !== null ? request : {}) as {
463
+ expectedRevision?: unknown;
464
+ expectedWorkspaceRevision?: unknown;
465
+ };
466
+ return {
467
+ task: value.expectedRevision === undefined,
468
+ workspace: value.expectedWorkspaceRevision === undefined,
469
+ };
470
+ };
471
+ /**
472
+ * A store compare-and-swap rejection (it carries the actual revision) on a
473
+ * revision the engine filled. A conflict on a caller-supplied revision, or a
474
+ * semantic conflict such as a changed import source, is never retried.
475
+ */
476
+ const filledConflict = (result: Result<unknown>, filled: FilledRevisions): boolean =>
477
+ !result.ok &&
478
+ result.code === "revision_conflict" &&
479
+ (("actualRevision" in result.details && filled.task) ||
480
+ ("actualWorkspaceRevision" in result.details && filled.workspace));
481
+ /**
482
+ * Re-run `run` while it loses a compare-and-swap race on an engine-filled
483
+ * revision. Each attempt commits at most once and a CAS rejection commits
484
+ * nothing, so a success is applied exactly once. Attempts stop at
485
+ * REVISION_RETRY_ATTEMPTS or once the lock budget has elapsed — so blocking
486
+ * stays within about twice that budget — and end as retryable busy.
487
+ */
488
+ const retryFilledRevisions = <T>(filled: FilledRevisions, run: () => Result<T>): Result<T> => {
489
+ if (!filled.task && !filled.workspace) return run();
490
+ let result = run();
491
+ // The deadline starts after the first attempt, so a slow first lock wait
492
+ // still leaves at least one retry.
493
+ const deadline = Date.now() + defaultLockTimeout();
494
+ for (let attempt = 1; filledConflict(result, filled); attempt += 1) {
495
+ if (attempt >= REVISION_RETRY_ATTEMPTS || Date.now() >= deadline)
496
+ return failure("busy", "records kept changing under concurrent writers; retry the call");
497
+ // Jittered backoff de-synchronizes writers that lost the same race.
498
+ Atomics.wait(retryPause, 0, 0, Math.floor(Math.random() * 4 * attempt) + 1);
499
+ result = run();
500
+ }
501
+ return result;
502
+ };
503
+
455
504
  const trustedNow = (context: OperationContext): Utc =>
456
505
  typeof context.now === "function" ? context.now() : context.now;
457
506
 
@@ -747,6 +796,17 @@ export class WorkitCore {
747
796
  input.expectedWorkspaceRevision = workspace ? workspace.revision : null;
748
797
  }
749
798
 
799
+ /**
800
+ * A revision the caller omitted is not a compare-and-swap request: when
801
+ * another writer commits between this call's read and its locked commit,
802
+ * re-run the whole operation — fresh read, policy, requirement and
803
+ * candidate checks, then commit — so a re-check that now fails returns that
804
+ * failure. Caller-supplied revisions stay strict CAS.
805
+ */
806
+ private retryOmittedRevisions<T>(request: unknown, run: () => Result<T>): Result<T> {
807
+ return retryFilledRevisions(filledRevisions(request), run);
808
+ }
809
+
750
810
  private helperEntry(
751
811
  task: TaskRecord,
752
812
  requireSession = false,
@@ -786,6 +846,15 @@ export class WorkitCore {
786
846
  }
787
847
 
788
848
  state(request: unknown): Result<ExportBundle | TaskSummary | TaskRecord | WorkspaceRecord> {
849
+ // Recovery is an operator CAS over snapshot bytes; only import retries.
850
+ return (request as { action?: unknown } | null)?.action === "import"
851
+ ? this.retryOmittedRevisions(request, () => this.stateOnce(request))
852
+ : this.stateOnce(request);
853
+ }
854
+
855
+ private stateOnce(
856
+ request: unknown,
857
+ ): Result<ExportBundle | TaskSummary | TaskRecord | WorkspaceRecord> {
789
858
  const root = this.contextRootError();
790
859
  if (!root.ok) return root;
791
860
  if ((this.context.workerId ?? null) !== null)
@@ -890,6 +959,10 @@ export class WorkitCore {
890
959
  }
891
960
 
892
961
  task(request: unknown): Result<TaskSummary | TaskListItem[] | TaskView> {
962
+ return this.retryOmittedRevisions(request, () => this.taskOnce(request));
963
+ }
964
+
965
+ private taskOnce(request: unknown): Result<TaskSummary | TaskListItem[] | TaskView> {
893
966
  const root = this.contextRootError();
894
967
  if (!root.ok) return root;
895
968
  const parsed = parseOperation("task", request);
@@ -997,6 +1070,10 @@ export class WorkitCore {
997
1070
  }
998
1071
 
999
1072
  policy(request: unknown): Result<Policy | null> {
1073
+ return this.retryOmittedRevisions(request, () => this.policyOnce(request));
1074
+ }
1075
+
1076
+ private policyOnce(request: unknown): Result<Policy | null> {
1000
1077
  const root = this.contextRootError();
1001
1078
  if (!root.ok) return root;
1002
1079
  const parsed = parseOperation("policy", request);
@@ -1049,6 +1126,10 @@ export class WorkitCore {
1049
1126
  }
1050
1127
 
1051
1128
  evidence(request: unknown): Result<Entry<Evidence>> {
1129
+ return this.retryOmittedRevisions(request, () => this.evidenceOnce(request));
1130
+ }
1131
+
1132
+ private evidenceOnce(request: unknown): Result<Entry<Evidence>> {
1052
1133
  const root = this.contextRootError();
1053
1134
  if (!root.ok) return root;
1054
1135
  const parsed = parseOperation("evidence", request);
@@ -1176,6 +1257,36 @@ export class WorkitCore {
1176
1257
  return this.recordDecision(request, undefined, true, true);
1177
1258
  }
1178
1259
 
1260
+ /**
1261
+ * Commit a task mutation whose update re-validates everything it depends on
1262
+ * under the lock. Decisions use this instead of a whole-operation retry
1263
+ * because a native receipt is retired before the commit and cannot be
1264
+ * verified twice: a CAS loss on an engine-filled revision re-reads the
1265
+ * record, repeats `recheck` against it and retries the commit; the update's
1266
+ * in-lock receipt check keeps a receipt from being consumed twice.
1267
+ */
1268
+ private commitTask(
1269
+ taskId: string,
1270
+ expected: string,
1271
+ filled: boolean,
1272
+ recheck: (fresh: TaskRecord) => Result<unknown>,
1273
+ update: Parameters<TaskStore["mutateTask"]>[2],
1274
+ ): Result<TaskRecord> {
1275
+ let revision = expected;
1276
+ let first = true;
1277
+ return retryFilledRevisions({ task: filled, workspace: false }, () => {
1278
+ if (!first) {
1279
+ const fresh = this.store.readTask(taskId);
1280
+ if (!fresh.ok) return fresh;
1281
+ const checked = recheck(fresh.data);
1282
+ if (!checked.ok) return checked;
1283
+ revision = fresh.data.revision;
1284
+ }
1285
+ first = false;
1286
+ return this.store.mutateTask(taskId, revision, update, trustedNow(this.context));
1287
+ });
1288
+ }
1289
+
1179
1290
  private recordDecision(
1180
1291
  request: unknown,
1181
1292
  nativeObservation?: unknown,
@@ -1214,6 +1325,7 @@ export class WorkitCore {
1214
1325
  if (!task.ok) return task;
1215
1326
  if ((this.context.workerId ?? null) !== null)
1216
1327
  return failure("permission_denied", "helpers cannot record or revoke decisions");
1328
+ const filled = input.expectedRevision === undefined;
1217
1329
  this.fillRevisions(input, task.data);
1218
1330
  if (input.action === "record") {
1219
1331
  if (task.data.status === "closed")
@@ -1269,10 +1381,31 @@ export class WorkitCore {
1269
1381
  const nativeProvenance = native?.ok ? retireNativeAuthority(native.data) : null;
1270
1382
  if (native?.ok && !nativeProvenance)
1271
1383
  return failure("permission_denied", "native decision authority was retired");
1272
- const changed = this.store.mutateTask(
1384
+ // A retry repeats the pre-lock checks that depend on the task record and
1385
+ // records the provenance of the standing re-verification it passed;
1386
+ // closure, receipt reuse, content and requirements are re-checked in the lock.
1387
+ let standingProvenance = standingApproval?.ok === true ? standingApproval.data : null;
1388
+ const recheck = (fresh: TaskRecord): Result<unknown> => {
1389
+ if (fresh.status === "closed")
1390
+ return failure("invalid_transition", "closed task cannot record a decision");
1391
+ if (!standingProvenance) return success(null, null, null);
1392
+ const again = verifyStandingApproval(
1393
+ this.store.root,
1394
+ fresh,
1395
+ this.context.caller,
1396
+ input.binding,
1397
+ );
1398
+ if (again.ok) standingProvenance = again.data;
1399
+ return again;
1400
+ };
1401
+ const changed = this.commitTask(
1273
1402
  task.data.id,
1274
1403
  input.expectedRevision,
1404
+ filled,
1405
+ recheck,
1275
1406
  (current, mutation) => {
1407
+ if (current.status === "closed")
1408
+ return failure("invalid_transition", "closed task cannot record a decision");
1276
1409
  const latestContent = verifyDecisionContent(this.store, input.binding);
1277
1410
  if (!latestContent.ok) return latestContent;
1278
1411
  const known = new Set(current.policy?.requirements.map((item) => item.id) ?? []);
@@ -1293,9 +1426,7 @@ export class WorkitCore {
1293
1426
  id: newId(),
1294
1427
  recordedAt: mutation.now,
1295
1428
  provenance:
1296
- standingApproval?.ok === true
1297
- ? standingApproval.data
1298
- : (nativeProvenance ?? provenance(this.context, "agent_reported")),
1429
+ standingProvenance ?? nativeProvenance ?? provenance(this.context, "agent_reported"),
1299
1430
  data,
1300
1431
  };
1301
1432
  return success(mutation.revision, null, {
@@ -1303,7 +1434,6 @@ export class WorkitCore {
1303
1434
  decisions: [...current.decisions, entry],
1304
1435
  });
1305
1436
  },
1306
- trustedNow(this.context),
1307
1437
  );
1308
1438
  if (!changed.ok) return changed;
1309
1439
  return success(changed.data.revision, null, changed.data.decisions.at(-1)!);
@@ -1315,9 +1445,11 @@ export class WorkitCore {
1315
1445
  if (decision.data.revoked) return failure("invalid_transition", "decision is already revoked");
1316
1446
  if (decision.data.consumption)
1317
1447
  return failure("permission_denied", "consumed decision cannot be revoked");
1318
- const changed = this.store.mutateTask(
1448
+ const changed = this.commitTask(
1319
1449
  task.data.id,
1320
1450
  input.expectedRevision,
1451
+ filled,
1452
+ () => success(null, null, null),
1321
1453
  (current, mutation) => {
1322
1454
  const entry = current.decisions.find((candidate) => candidate.id === input.decisionId);
1323
1455
  if (!entry) return failure("not_found", "decision not found");
@@ -1336,7 +1468,6 @@ export class WorkitCore {
1336
1468
  ),
1337
1469
  });
1338
1470
  },
1339
- trustedNow(this.context),
1340
1471
  );
1341
1472
  if (!changed.ok) return changed;
1342
1473
  const entry = changed.data.decisions.find((candidate) => candidate.id === input.decisionId);
@@ -1346,6 +1477,10 @@ export class WorkitCore {
1346
1477
  }
1347
1478
 
1348
1479
  finding(request: unknown): Result<Entry<Finding>> {
1480
+ return this.retryOmittedRevisions(request, () => this.findingOnce(request));
1481
+ }
1482
+
1483
+ private findingOnce(request: unknown): Result<Entry<Finding>> {
1349
1484
  const root = this.contextRootError();
1350
1485
  if (!root.ok) return root;
1351
1486
  const parsed = parseOperation("finding", request);
@@ -1515,6 +1650,10 @@ export class WorkitCore {
1515
1650
  }
1516
1651
 
1517
1652
  worker(request: unknown): Result<Entry<Worker>> {
1653
+ return this.retryOmittedRevisions(request, () => this.workerOnce(request));
1654
+ }
1655
+
1656
+ private workerOnce(request: unknown): Result<Entry<Worker>> {
1518
1657
  const root = this.contextRootError();
1519
1658
  if (!root.ok) return root;
1520
1659
  const parsed = parseOperation("worker", request);
@@ -1950,6 +2089,10 @@ export class WorkitCore {
1950
2089
  }
1951
2090
 
1952
2091
  writer(request: unknown): Result<WorkspaceRecord> {
2092
+ return this.retryOmittedRevisions(request, () => this.writerOnce(request));
2093
+ }
2094
+
2095
+ private writerOnce(request: unknown): Result<WorkspaceRecord> {
1953
2096
  const root = this.contextRootError();
1954
2097
  if (!root.ok) return root;
1955
2098
  const parsed = parseOperation("writer", request);