@knpkv/herdr-work 0.10.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/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 an expected head can raise it; a plain append never checks one.
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 checkpoint. With `mode` `check`, every check runs in the same
2810
- * transaction and it is rolled back instead of written: the checkpoint is
2811
- * returned when it would have been appended.
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 decoded = yield* Schema.decodeUnknownEffect(WorkGoalCheckpoint)(event).pipe(Effect.mapError(storeError("append.decode")));
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 && decoded.eventId.startsWith(reconcilerEventPrefix)) {
2826
+ if (guard === null && input.eventId.startsWith(reconcilerEventPrefix)) {
2821
2827
  return yield* new WorkProjectionError({
2822
- cause: decoded.eventId,
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 decoded;
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 decoded;
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
  })));