@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.
- package/package.json +2 -1
- package/src/core/doctor.ts +45 -0
- package/src/core/methods.ts +7 -0
- package/src/core/store-lock.ts +301 -0
- package/src/core/task-contract.ts +121 -1
- package/src/core/task-engine.ts +150 -7
- package/src/core/task-store.ts +406 -79
- package/src/core.ts +1 -1
- package/src/git/rev.ts +736 -0
- package/src/{core/session-context.ts → hooks/context.ts} +95 -8
- package/src/hooks/descriptor.ts +113 -0
- package/src/hooks/handle.ts +111 -0
- package/src/hooks/hosts/claude-code.ts +281 -0
- package/src/hooks/hosts/codex.ts +298 -0
- package/src/hooks/hosts/cursor.ts +321 -0
- package/src/hooks/hosts/fields.ts +55 -0
- package/src/hooks/hosts/opencode.ts +79 -0
- package/src/hooks/hosts/pi.ts +80 -0
- package/src/hooks/index.ts +43 -0
- package/src/hooks/policy.ts +14 -0
- package/src/hooks/protocol.ts +89 -0
- package/src/hooks/run.ts +52 -0
package/src/core/task-engine.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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);
|