@knpkv/herdr-work 0.11.0 → 0.12.0
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/README.md +3 -1
- package/dist/errors.d.ts +8 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +7 -0
- package/dist/errors.js.map +1 -1
- package/dist/model.d.ts +13 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +11 -0
- package/dist/model.js.map +1 -1
- package/dist/service.d.ts +9 -2
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +2 -0
- package/dist/service.js.map +1 -1
- package/dist/store.d.ts +11 -12
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +83 -22
- package/dist/store.js.map +1 -1
- package/dist/view.d.ts +3 -0
- package/dist/view.d.ts.map +1 -1
- package/dist/view.js +18 -9
- package/dist/view.js.map +1 -1
- package/package.json +2 -2
package/dist/store.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { agentConnectTarget, workAbandonActivitySummary, workReassignActivitySummary } from "@knpkv/herdr-fleet";
|
|
2
2
|
import { openPrivateSqlite } from "@knpkv/herdr-fleet/sqlite";
|
|
3
|
-
import { Clock, Crypto, Effect, Equal, Option, Result, Schema } from "effect";
|
|
3
|
+
import { Clock, Crypto, Effect, Equal, Option, Predicate, Result, Schema } from "effect";
|
|
4
4
|
import { Hex } from "effect/encoding";
|
|
5
5
|
import { makeWorkAgentBinding } from "./agent-binding.js";
|
|
6
|
-
import { WorkAdmissionConflictError, WorkAgentBindingAuthorityError, WorkAgentBindingConflictError, WorkCheckpointConflictError, WorkCoordinatorHandoffConflictError, WorkDecisionAuthorityConflictError, WorkDecisionHandoffConflictError, WorkDecisionRevisionConflictError, WorkGoalAbandonmentConflictError, WorkGoalAgentTargetConflictError, WorkGoalBindingRequiresAgentError, WorkGoalLaneActiveError, WorkGoalOwnerMismatchError, WorkGoalReassignmentConflictError, WorkGoalRevisionConflictError, WorkGoalTerminalError, WorkLaneClaimConflictError, WorkLaneGoalConflictError, WorkLaneOperationConflictError, WorkProjectionError, WorkPullRequestLinkError, WorkStoreError, WorkTransactionConflictError } from "./errors.js";
|
|
6
|
+
import { WorkAdmissionConflictError, WorkAgentBindingAuthorityError, WorkAgentBindingConflictError, WorkCheckpointConflictError, WorkCoordinatorHandoffConflictError, WorkDecisionAuthorityConflictError, WorkDecisionHandoffConflictError, WorkDecisionRevisionConflictError, WorkGoalAbandonmentConflictError, WorkGoalAgentTargetConflictError, WorkGoalBindingRequiresAgentError, WorkGoalLaneActiveError, WorkGoalOwnerMismatchError, WorkGoalReassignmentConflictError, WorkGoalRevisionConflictError, WorkGoalTerminalError, WorkLaneClaimConflictError, WorkLaneGoalConflictError, WorkLaneOperationConflictError, WorkProjectionError, WorkPullRequestAttachConflict, WorkPullRequestLinkError, WorkStoreError, WorkTransactionConflictError } from "./errors.js";
|
|
7
7
|
import { validateGoalFamilyHistory } from "./goal-family.js";
|
|
8
8
|
import { agentBindingAdmissionError, workAgentBindingLaneOperationMaxBytes, workAgentBindingLaneOperationMaxRecords } from "./internal/agent-binding-admission.js";
|
|
9
9
|
import { AgentBindingGoalEventRow, AgentBindingLaneOperationRow, agentBindingReadbackError, AgentBindingRow, decodeAgentBindingGoalEvent, decodeAgentBindingRow, laneOperationReadbackError } from "./internal/agent-binding-readback.js";
|
|
@@ -13,7 +13,7 @@ import { workHistoryError } from "./internal/history-validation.js";
|
|
|
13
13
|
import { LaneOperationLedgerRow, LaneOperationTotalsRow, planLegacyLaneOperations, resolveLegacyLaneClaim } from "./internal/legacy-lane-claim.js";
|
|
14
14
|
import { enforcesOneHandoffPerSession, SessionIndexDefinition, sessionIndexDefinitionQuery } from "./internal/session-index.js";
|
|
15
15
|
import { goalActivityLimit, terminalCandidates, terminalCheckpoint, withoutBlockers } from "./internal/terminal-reconcile.js";
|
|
16
|
-
import { admissionEvidence, isTerminalWorkState, WorkAdmissionTarget, WorkAgentBinding, WorkAgentBindingRequest, WorkCoordinatorSessionId, WorkDecisionHandoff, WorkDispatchHandoff, WorkExistingGoalRecovery, WorkGoal, WorkGoalAbandoned, WorkGoalAbandonment, WorkGoalCheckpoint, WorkGoalId, WorkGoalReassigned, WorkGoalReassignment, workHistoryMaxEvents, WorkLaneClaim, WorkLaneClaimed, WorkObservationEnvelope, WorkObservationId, workObservationMaxSkewMillis, WorkObservationSubject, WorkObservedAdmission, WorkObservedFact, workObservedFactMaxBytes, workObservedFactMaxRecords, WorkObservedFailure, WorkProspectiveAdmission, WorkPullRequestLink, WorkReconcileOptions, workReconcilerHeadroom, WorkRecoveryTarget, workSnapshotMaxGoals } from "./model.js";
|
|
16
|
+
import { admissionEvidence, isTerminalWorkState, WorkAdmissionTarget, WorkAgentBinding, WorkAgentBindingRequest, WorkCoordinatorSessionId, WorkDecisionHandoff, WorkDispatchHandoff, WorkExistingGoalRecovery, WorkGoal, WorkGoalAbandoned, WorkGoalAbandonment, WorkGoalCheckpoint, WorkGoalId, WorkGoalReassigned, WorkGoalReassignment, workHistoryMaxEvents, WorkLaneClaim, WorkLaneClaimed, WorkObservationEnvelope, WorkObservationId, workObservationMaxSkewMillis, WorkObservationSubject, WorkObservedAdmission, WorkObservedFact, workObservedFactMaxBytes, workObservedFactMaxRecords, WorkObservedFailure, WorkProspectiveAdmission, WorkPullRequestAttach, WorkPullRequestLink, WorkReconcileOptions, workReconcilerHeadroom, WorkRecoveryTarget, workSnapshotMaxGoals } from "./model.js";
|
|
17
17
|
import { asciiLower, canonicalSubject, observationSubject, reconcilerEventPrefix } from "./observed.js";
|
|
18
18
|
const StoredEventRow = Schema.Struct({ record: Schema.String });
|
|
19
19
|
const StoredEventRows = Schema.Array(StoredEventRow);
|
|
@@ -2796,30 +2796,36 @@ export class WorkStore {
|
|
|
2796
2796
|
const entries = yield* Schema.decodeUnknownEffect(Schema.Array(Schema.Struct({ dispatchRequestId: WorkAgentBindingRequest.fields.dispatchRequestId })))(rows).pipe(Effect.mapError(storeError("agent-binding.lane.rows")));
|
|
2797
2797
|
return entries.map(({ dispatchRequestId }) => dispatchRequestId);
|
|
2798
2798
|
});
|
|
2799
|
+
/** Builds the checkpoint from the stored head under the append transaction's lock. */
|
|
2800
|
+
attachPullRequest = Effect.fn("WorkStore.attachPullRequest")(function* (input) {
|
|
2801
|
+
const decoded = yield* Schema.decodeUnknownEffect(WorkPullRequestAttach, { onExcessProperty: "error" })(input)
|
|
2802
|
+
.pipe(Effect.mapError(storeError("attachPullRequest.decode")));
|
|
2803
|
+
return yield* this.appendAt(decoded, null).pipe(Effect.catchTag("WorkGoalRevisionConflictError", Effect.die));
|
|
2804
|
+
});
|
|
2799
2805
|
append = (event) => this.appendAt(event, null).pipe(
|
|
2800
|
-
// Only
|
|
2801
|
-
Effect.catchTag("WorkGoalRevisionConflictError", Effect.die));
|
|
2802
|
-
/**
|
|
2803
|
-
* Appends one checkpoint. With `expectedHead`, the goal's latest checkpoint
|
|
2804
|
-
* must still be that one when the write commits (an exact replay always
|
|
2805
|
-
* succeeds), so a writer that planned from the head cannot bury a newer
|
|
2806
|
-
* owner checkpoint.
|
|
2807
|
-
*/
|
|
2806
|
+
// Only guarded writes can raise these; a plain append checks neither.
|
|
2807
|
+
Effect.catchTag(["WorkGoalRevisionConflictError", "WorkPullRequestAttachConflict"], Effect.die));
|
|
2808
2808
|
/**
|
|
2809
|
-
* Appends one
|
|
2810
|
-
*
|
|
2811
|
-
*
|
|
2809
|
+
* Appends under one transaction. A reconciler guard compares the stored head
|
|
2810
|
+
* and observed fact; an attachment derives its checkpoint from the stored
|
|
2811
|
+
* head after checking the caller's expected event and repository identity.
|
|
2812
|
+
* Same-URL attachments and exact checkpoint replays are no-op successes.
|
|
2813
|
+
* In `check` mode every check runs, then rolls back instead of writing.
|
|
2812
2814
|
*/
|
|
2813
2815
|
appendAt = Effect.fn("WorkStore.append")(function* (event, guard, mode = "write",
|
|
2814
2816
|
// In a check, the checkpoints earlier steps of the same plan would have
|
|
2815
2817
|
// written: limits are checked as if they were already stored.
|
|
2816
2818
|
planned = []) {
|
|
2817
|
-
const
|
|
2819
|
+
const decode = "goal" in event
|
|
2820
|
+
? Schema.decodeUnknownEffect(WorkGoalCheckpoint)(event).pipe(Effect.mapError(storeError("append.decode")))
|
|
2821
|
+
: Schema.decodeUnknownEffect(WorkPullRequestAttach, { onExcessProperty: "error" })(event)
|
|
2822
|
+
.pipe(Effect.mapError(storeError("append.decode")));
|
|
2823
|
+
const input = yield* decode;
|
|
2818
2824
|
// The reconciler's event and activity ids are its provenance; no other
|
|
2819
2825
|
// writer may use them.
|
|
2820
|
-
if (guard === null &&
|
|
2826
|
+
if (guard === null && input.eventId.startsWith(reconcilerEventPrefix)) {
|
|
2821
2827
|
return yield* new WorkProjectionError({
|
|
2822
|
-
cause:
|
|
2828
|
+
cause: input.eventId,
|
|
2823
2829
|
detail: `event ids starting with ${reconcilerEventPrefix} are reserved for the reconciler`,
|
|
2824
2830
|
reason: "malformed"
|
|
2825
2831
|
});
|
|
@@ -2837,6 +2843,61 @@ export class WorkStore {
|
|
|
2837
2843
|
transaction = false;
|
|
2838
2844
|
return { _tag: "rejected", error };
|
|
2839
2845
|
};
|
|
2846
|
+
const decoded = (() => {
|
|
2847
|
+
if ("goal" in input)
|
|
2848
|
+
return input;
|
|
2849
|
+
const headRow = this.#database.prepare(`SELECT record FROM work_goal_events WHERE goal_id = ?
|
|
2850
|
+
ORDER BY occurred_at DESC, event_id DESC LIMIT 1`).get(input.goalId);
|
|
2851
|
+
const head = headRow === undefined ? null : Schema.decodeUnknownSync(WorkGoalCheckpoint)(JSON.parse(Schema.decodeUnknownSync(StoredEventRow)(headRow).record));
|
|
2852
|
+
const conflict = (reason) => new WorkPullRequestAttachConflict({
|
|
2853
|
+
goalId: input.goalId,
|
|
2854
|
+
expectedGoalEventId: input.expectedGoalEventId,
|
|
2855
|
+
actualGoalEventId: head?.eventId ?? null,
|
|
2856
|
+
reason
|
|
2857
|
+
});
|
|
2858
|
+
if (head === null)
|
|
2859
|
+
return conflict("missing_goal");
|
|
2860
|
+
if (head.goal.repository.branch !== input.branch ||
|
|
2861
|
+
head.goal.repository.repository !== input.repository)
|
|
2862
|
+
return conflict("identity_mismatch");
|
|
2863
|
+
// A retry returns the stored checkpoint, preserving any later owner updates.
|
|
2864
|
+
if (head.goal.review?.url === input.reviewUrl) {
|
|
2865
|
+
return { _tag: "replayed", event: head };
|
|
2866
|
+
}
|
|
2867
|
+
if (head.eventId !== input.expectedGoalEventId)
|
|
2868
|
+
return conflict("stale_event");
|
|
2869
|
+
if (isTerminalWorkState(head.goal.state) || head.goal.goalFamily?.role === "superseded") {
|
|
2870
|
+
return conflict("inactive_goal");
|
|
2871
|
+
}
|
|
2872
|
+
if (head.goal.review?.url != null)
|
|
2873
|
+
return conflict("already_attached");
|
|
2874
|
+
if (input.occurredAt <= head.occurredAt) {
|
|
2875
|
+
return new WorkProjectionError({
|
|
2876
|
+
cause: input,
|
|
2877
|
+
detail: "pull request attachment must advance the goal timestamp",
|
|
2878
|
+
reason: "inconsistent_history"
|
|
2879
|
+
});
|
|
2880
|
+
}
|
|
2881
|
+
return Schema.decodeUnknownSync(WorkGoalCheckpoint)({
|
|
2882
|
+
version: "herdr.work.event.v1",
|
|
2883
|
+
eventId: input.eventId,
|
|
2884
|
+
occurredAt: input.occurredAt,
|
|
2885
|
+
goal: {
|
|
2886
|
+
...head.goal,
|
|
2887
|
+
updatedAt: input.occurredAt,
|
|
2888
|
+
review: { state: "requested", url: input.reviewUrl, summary: null, updatedAt: input.occurredAt }
|
|
2889
|
+
}
|
|
2890
|
+
});
|
|
2891
|
+
})();
|
|
2892
|
+
if (Predicate.isTagged(decoded, "WorkPullRequestAttachConflict") ||
|
|
2893
|
+
Predicate.isTagged(decoded, "WorkProjectionError")) {
|
|
2894
|
+
return reject(decoded);
|
|
2895
|
+
}
|
|
2896
|
+
if (Predicate.hasProperty(decoded, "_tag")) {
|
|
2897
|
+
abort();
|
|
2898
|
+
transaction = false;
|
|
2899
|
+
return { _tag: "replayed", event: decoded.event };
|
|
2900
|
+
}
|
|
2840
2901
|
const collisions = Schema.decodeUnknownSync(StoredEventRows)(this.#database
|
|
2841
2902
|
.prepare(`SELECT record FROM work_goal_events
|
|
2842
2903
|
WHERE event_id = ? OR (goal_id = ? AND occurred_at = ?)`)
|
|
@@ -2921,7 +2982,7 @@ export class WorkStore {
|
|
|
2921
2982
|
if (mode === "check") {
|
|
2922
2983
|
abort();
|
|
2923
2984
|
transaction = false;
|
|
2924
|
-
return { _tag: "checked" };
|
|
2985
|
+
return { _tag: "checked", event: decoded };
|
|
2925
2986
|
}
|
|
2926
2987
|
const result = this.#database.prepare("INSERT INTO work_goal_events (event_id, goal_id, occurred_at, record) VALUES (?, ?, ?, ?)").run(decoded.eventId, decoded.goal.id, decoded.occurredAt, JSON.stringify(decoded));
|
|
2927
2988
|
// The reconciler's own record of what it wrote: its authorship and its
|
|
@@ -2931,7 +2992,7 @@ export class WorkStore {
|
|
|
2931
2992
|
}
|
|
2932
2993
|
this.#database.exec("COMMIT");
|
|
2933
2994
|
transaction = false;
|
|
2934
|
-
return { _tag: "inserted", changes: result.changes };
|
|
2995
|
+
return { _tag: "inserted", changes: result.changes, event: decoded };
|
|
2935
2996
|
}
|
|
2936
2997
|
catch (error) {
|
|
2937
2998
|
if (transaction)
|
|
@@ -2946,11 +3007,11 @@ export class WorkStore {
|
|
|
2946
3007
|
if (decision._tag === "replayed")
|
|
2947
3008
|
return decision.event;
|
|
2948
3009
|
if (decision._tag === "checked")
|
|
2949
|
-
return
|
|
3010
|
+
return decision.event;
|
|
2950
3011
|
if (decision.changes !== 1 && decision.changes !== 1n) {
|
|
2951
3012
|
return yield* storeError("append.insert.count")(decision.changes);
|
|
2952
3013
|
}
|
|
2953
|
-
return
|
|
3014
|
+
return decision.event;
|
|
2954
3015
|
});
|
|
2955
3016
|
appendMany = Effect.fn("WorkStore.appendMany")(function* (transactionId, events) {
|
|
2956
3017
|
if (events.length > workHistoryMaxEvents) {
|
|
@@ -4038,7 +4099,7 @@ export class WorkStore {
|
|
|
4038
4099
|
// A plan holds no transaction between steps: each check rolls back at once,
|
|
4039
4100
|
// and later steps are checked against the checkpoints earlier ones planned.
|
|
4040
4101
|
const planned = [];
|
|
4041
|
-
const appendAt = (checkpoint, guard) => this.appendAt(checkpoint, guard, mode, planned).pipe(Effect.tap(() => Effect.sync(() => {
|
|
4102
|
+
const appendAt = (checkpoint, guard) => this.appendAt(checkpoint, guard, mode, planned).pipe(Effect.catchTag("WorkPullRequestAttachConflict", Effect.die), Effect.tap(() => Effect.sync(() => {
|
|
4042
4103
|
if (mode === "check")
|
|
4043
4104
|
planned.push(checkpoint);
|
|
4044
4105
|
})));
|