things-api 0.19.3 → 0.20.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.
Files changed (139) hide show
  1. package/README.md +1 -1
  2. package/deputy/prebuilt/Things API Helper.app/Contents/CodeResources +0 -0
  3. package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/MacOS/things-reader +0 -0
  4. package/deputy/prebuilt/Things API Helper.app/Contents/MacOS/things-deputy +0 -0
  5. package/dist/audit/schema.d.ts +42 -10
  6. package/dist/audit/schema.js +21 -0
  7. package/dist/audit/schema.js.map +1 -1
  8. package/dist/cli/commands/doctor.js +3 -1
  9. package/dist/cli/commands/doctor.js.map +1 -1
  10. package/dist/cli/commands/op-result.js +25 -7
  11. package/dist/cli/commands/op-result.js.map +1 -1
  12. package/dist/cli/commands/repeat-flags.d.ts +19 -1
  13. package/dist/cli/commands/repeat-flags.js +71 -0
  14. package/dist/cli/commands/repeat-flags.js.map +1 -1
  15. package/dist/cli/commands/rescue.d.ts +12 -0
  16. package/dist/cli/commands/rescue.js +83 -0
  17. package/dist/cli/commands/rescue.js.map +1 -0
  18. package/dist/cli/commands/writes.js +86 -32
  19. package/dist/cli/commands/writes.js.map +1 -1
  20. package/dist/cli/help.js +25 -25
  21. package/dist/cli/help.js.map +1 -1
  22. package/dist/cli/main.js +12 -2
  23. package/dist/cli/main.js.map +1 -1
  24. package/dist/cli/resolve-invocation.d.ts +9 -1
  25. package/dist/cli/resolve-invocation.js +71 -0
  26. package/dist/cli/resolve-invocation.js.map +1 -1
  27. package/dist/cli/unknown-command.d.ts +26 -0
  28. package/dist/cli/unknown-command.js +135 -0
  29. package/dist/cli/unknown-command.js.map +1 -0
  30. package/dist/contracts.d.ts +4 -3
  31. package/dist/contracts.js +1 -1
  32. package/dist/index.d.ts +4 -2
  33. package/dist/index.js +1 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/mcp/server.js +62 -10
  36. package/dist/mcp/server.js.map +1 -1
  37. package/dist/model/entities.d.ts +11 -0
  38. package/dist/model/entities.js.map +1 -1
  39. package/dist/model/mappers.js +15 -0
  40. package/dist/model/mappers.js.map +1 -1
  41. package/dist/op-result.d.ts +24 -2
  42. package/dist/op-result.js +77 -20
  43. package/dist/op-result.js.map +1 -1
  44. package/dist/process-instance.d.ts +41 -0
  45. package/dist/process-instance.js +82 -0
  46. package/dist/process-instance.js.map +1 -0
  47. package/dist/rescue.d.ts +247 -0
  48. package/dist/rescue.js +811 -0
  49. package/dist/rescue.js.map +1 -0
  50. package/dist/session-grant.js +4 -15
  51. package/dist/session-grant.js.map +1 -1
  52. package/dist/ui-state.d.ts +8 -4
  53. package/dist/ui-state.js +8 -4
  54. package/dist/ui-state.js.map +1 -1
  55. package/dist/write/batch.js +4 -1
  56. package/dist/write/batch.js.map +1 -1
  57. package/dist/write/clone.js +12 -6
  58. package/dist/write/clone.js.map +1 -1
  59. package/dist/write/commands.d.ts +9 -0
  60. package/dist/write/commands.js +135 -50
  61. package/dist/write/commands.js.map +1 -1
  62. package/dist/write/disclosures.d.ts +275 -0
  63. package/dist/write/disclosures.js +277 -0
  64. package/dist/write/disclosures.js.map +1 -0
  65. package/dist/write/experimental.d.ts +16 -0
  66. package/dist/write/experimental.js +19 -0
  67. package/dist/write/experimental.js.map +1 -1
  68. package/dist/write/failure-hints.js +3 -2
  69. package/dist/write/failure-hints.js.map +1 -1
  70. package/dist/write/guards.d.ts +1 -1
  71. package/dist/write/guards.js +102 -3
  72. package/dist/write/guards.js.map +1 -1
  73. package/dist/write/heading.js +4 -7
  74. package/dist/write/heading.js.map +1 -1
  75. package/dist/write/lock.d.ts +85 -1
  76. package/dist/write/lock.js +121 -6
  77. package/dist/write/lock.js.map +1 -1
  78. package/dist/write/make-repeating-project.js +2 -1
  79. package/dist/write/make-repeating-project.js.map +1 -1
  80. package/dist/write/operations.d.ts +52 -6
  81. package/dist/write/operations.js +22 -0
  82. package/dist/write/operations.js.map +1 -1
  83. package/dist/write/opid.d.ts +69 -8
  84. package/dist/write/opid.js +116 -20
  85. package/dist/write/opid.js.map +1 -1
  86. package/dist/write/param-schema.js +14 -2
  87. package/dist/write/param-schema.js.map +1 -1
  88. package/dist/write/pipeline.d.ts +82 -3
  89. package/dist/write/pipeline.js +204 -60
  90. package/dist/write/pipeline.js.map +1 -1
  91. package/dist/write/planner.d.ts +7 -0
  92. package/dist/write/planner.js +12 -0
  93. package/dist/write/planner.js.map +1 -1
  94. package/dist/write/preserve-modified.d.ts +17 -8
  95. package/dist/write/preserve-modified.js.map +1 -1
  96. package/dist/write/promote-clone.js +106 -53
  97. package/dist/write/promote-clone.js.map +1 -1
  98. package/dist/write/reorder.js +45 -39
  99. package/dist/write/reorder.js.map +1 -1
  100. package/dist/write/repeat-rule.d.ts +25 -1
  101. package/dist/write/repeat-rule.js +69 -0
  102. package/dist/write/repeat-rule.js.map +1 -1
  103. package/dist/write/resolution-timestamps.js +104 -78
  104. package/dist/write/resolution-timestamps.js.map +1 -1
  105. package/dist/write/reversibility.js +2 -2
  106. package/dist/write/reversibility.js.map +1 -1
  107. package/dist/write/spawn-expectation.d.ts +135 -0
  108. package/dist/write/spawn-expectation.js +237 -0
  109. package/dist/write/spawn-expectation.js.map +1 -0
  110. package/dist/write/template-mutation.js +34 -35
  111. package/dist/write/template-mutation.js.map +1 -1
  112. package/dist/write/undo.js +45 -5
  113. package/dist/write/undo.js.map +1 -1
  114. package/dist/write/vectors/simulator.js +48 -2
  115. package/dist/write/vectors/simulator.js.map +1 -1
  116. package/dist/write/vectors/types.d.ts +12 -0
  117. package/dist/write/vectors/ui-recipes.js +11 -1
  118. package/dist/write/vectors/ui-recipes.js.map +1 -1
  119. package/dist/write/vectors/ui-state.d.ts +64 -6
  120. package/dist/write/vectors/ui-state.js +91 -4
  121. package/dist/write/vectors/ui-state.js.map +1 -1
  122. package/dist/write/vectors/ui.d.ts +69 -3
  123. package/dist/write/vectors/ui.js +293 -146
  124. package/dist/write/vectors/ui.js.map +1 -1
  125. package/dist/write/vectors/url-scheme.js +26 -0
  126. package/dist/write/vectors/url-scheme.js.map +1 -1
  127. package/dist/write/verify/delta.d.ts +17 -2
  128. package/dist/write/verify/delta.js +46 -16
  129. package/dist/write/verify/delta.js.map +1 -1
  130. package/dist/write/verify/poller.d.ts +3 -2
  131. package/dist/write/verify/poller.js +3 -1
  132. package/dist/write/verify/poller.js.map +1 -1
  133. package/package.json +1 -1
  134. package/schema/envelope.schema.json +1 -2
  135. package/skills/things-cli/SKILL.md +4 -2
  136. package/skills/things-cli/references/repeating.md +1 -1
  137. package/dist/cli/commands/ui-state.d.ts +0 -11
  138. package/dist/cli/commands/ui-state.js +0 -27
  139. package/dist/cli/commands/ui-state.js.map +0 -1
@@ -541,6 +541,51 @@ export interface RepeatRuleParams {
541
541
  */
542
542
  next?: IsoDate;
543
543
  }
544
+ /**
545
+ * `reschedule-repeat` params — the full {@link RepeatRuleParams} vocabulary with
546
+ * `frequency` + `interval` OPTIONAL, because the verb now has TWO spellings:
547
+ *
548
+ * - **Restate the rule** (both present): the Repeat dialog is driven, exactly as
549
+ * before — the ui vector, `dangerouslyDriveGui`, the whole rule rewritten.
550
+ * - **RE-ANCHOR** (`{ uuid, next }` alone, both absent): move the series' next
551
+ * occurrence to a date without restating its rule. One background `open` on
552
+ * the URL scheme, tier 0, no GUI-drive acknowledgement — the path REANCH1
553
+ * measured (docs/lab/reanch1-url-reanchor.md) and REANCH2 re-verified. It is
554
+ * guarded hard: Things ≥ 3.23, a strictly-future date, a fixed non-paused
555
+ * single-weekday rule, and a route matching the row type.
556
+ *
557
+ * The two are exclusive by construction — a re-anchor carries `next` and NOTHING
558
+ * else, so any other rule field means the dialog spelling and `frequency` +
559
+ * `interval` are required with it (enforced by `assertRescheduleRule`).
560
+ */
561
+ export interface RescheduleRepeatParams extends Omit<RepeatRuleParams, "frequency" | "interval"> {
562
+ /** The recurrence unit — required UNLESS this is a bare re-anchor. */
563
+ frequency?: RepeatFrequency;
564
+ /** "every N units", 1–99 — required UNLESS this is a bare re-anchor. */
565
+ interval?: number;
566
+ }
567
+ /**
568
+ * The bare RE-ANCHOR spelling of {@link RescheduleRepeatParams}: a target uuid and
569
+ * the date its series' next occurrence moves to, nothing else.
570
+ */
571
+ export interface RepeatReanchorParams {
572
+ uuid: string;
573
+ next: IsoDate;
574
+ }
575
+ /**
576
+ * Is this reschedule the bare re-anchor (URL-drivable) rather than a rule
577
+ * restatement (GUI-bound)? The discriminator is `frequency`: validation
578
+ * (`assertRescheduleRule`) refuses every other rule field alongside its absence,
579
+ * so an params bag without a frequency IS `{ uuid, next }`.
580
+ */
581
+ export declare function isRepeatReanchor(params: RescheduleRepeatParams): params is RepeatReanchorParams & RescheduleRepeatParams;
582
+ /**
583
+ * Does THIS call drive the GUI? Every {@link UI_DRIVE_OPS} member does, except a
584
+ * `reschedule-repeat` in its bare re-anchor spelling, which compiles to one URL
585
+ * dispatch and therefore carries no GUI-drive gate (H-UI-DRIVE). Keyed on the
586
+ * params, so the scare gate rides the SPELLING rather than the verb.
587
+ */
588
+ export declare function drivesGuiForParams(op: OperationKind, params: Record<string, unknown>): boolean;
544
589
  /**
545
590
  * The calendar-anchor subset of the Repeat-dialog rule vocabulary the
546
591
  * add-repeating composites carry on their PROMOTE leg (frequency/interval plus
@@ -617,7 +662,8 @@ export interface ProjectAddRepeatingParams extends AddRepeatingRuleFields {
617
662
  * landing on the seed — this deadlines every occurrence AND keeps the seed
618
663
  * deadline-free so the app does not SRCFATE-preserve it into a double-booked
619
664
  * future instance (DBLSPAWN1). It therefore requires a concrete `when` on or
620
- * before it (after-completion series keep the seed's one-off deadline instead).
665
+ * before it. An AFTER-COMPLETION series is deadlined the same way: the offset is
666
+ * measured from each occurrence's own start, which such a series has (CNCAC2).
621
667
  * `startDaysEarlier` is the SAME deadline geometry named from the other end (the
622
668
  * per-occurrence lead directly, deadline derived as `when + N`); the two are
623
669
  * over-determined together and must AGREE (`deadline − when == startDaysEarlier`).
@@ -631,7 +677,8 @@ export interface TodoAddRepeatingParams extends AddRepeatingRuleFields {
631
677
  /**
632
678
  * The FIRST occurrence's due date — mapped to the RULE (derive start-days-earlier =
633
679
  * deadline − when), so every occurrence is deadlined and the seed carries none.
634
- * Needs a concrete `when` on or before it; not applicable to after-completion.
680
+ * Needs a concrete `when` on or before it. Applies to an after-completion series
681
+ * too — the offset is measured from each occurrence's start, not from a calendar.
635
682
  */
636
683
  deadline?: IsoDate;
637
684
  /**
@@ -639,8 +686,7 @@ export interface TodoAddRepeatingParams extends AddRepeatingRuleFields {
639
686
  * before its own deadline (integer ≥ 0), the deadline derived as `when + N`.
640
687
  * Where a concrete `deadline` names the first due date, this names the
641
688
  * per-occurrence lead directly. Needs a concrete `when`; when BOTH are given they
642
- * must AGREE (`deadline − when == startDaysEarlier`) or the call is refused. Not
643
- * applicable to after-completion (no calendar start to count back from).
689
+ * must AGREE (`deadline − when == startDaysEarlier`) or the call is refused.
644
690
  */
645
691
  startDaysEarlier?: number;
646
692
  tags?: string[];
@@ -718,12 +764,12 @@ export interface OperationParamsMap {
718
764
  "project.dissolve-heading": UuidParams;
719
765
  "todo.clear-dated-reminder": UuidParams;
720
766
  "todo.make-repeating": RepeatRuleParams;
721
- "todo.reschedule-repeat": RepeatRuleParams;
767
+ "todo.reschedule-repeat": RescheduleRepeatParams;
722
768
  "todo.pause-repeat": UuidParams;
723
769
  "todo.resume-repeat": UuidParams;
724
770
  "todo.create-next-copy": UuidParams;
725
771
  "todo.convert-to-project": UuidParams;
726
- "project.reschedule-repeat": RepeatRuleParams;
772
+ "project.reschedule-repeat": RescheduleRepeatParams;
727
773
  "project.pause-repeat": UuidParams;
728
774
  "project.resume-repeat": UuidParams;
729
775
  "area.reorder": AreaReorderParams;
@@ -154,4 +154,26 @@ export const WEEKDAYS = [
154
154
  "friday",
155
155
  "saturday",
156
156
  ];
157
+ /**
158
+ * Is this reschedule the bare re-anchor (URL-drivable) rather than a rule
159
+ * restatement (GUI-bound)? The discriminator is `frequency`: validation
160
+ * (`assertRescheduleRule`) refuses every other rule field alongside its absence,
161
+ * so an params bag without a frequency IS `{ uuid, next }`.
162
+ */
163
+ export function isRepeatReanchor(params) {
164
+ return params.frequency === undefined && params.next !== undefined;
165
+ }
166
+ /**
167
+ * Does THIS call drive the GUI? Every {@link UI_DRIVE_OPS} member does, except a
168
+ * `reschedule-repeat` in its bare re-anchor spelling, which compiles to one URL
169
+ * dispatch and therefore carries no GUI-drive gate (H-UI-DRIVE). Keyed on the
170
+ * params, so the scare gate rides the SPELLING rather than the verb.
171
+ */
172
+ export function drivesGuiForParams(op, params) {
173
+ if (!isUiDriveOp(op))
174
+ return false;
175
+ if (op !== "todo.reschedule-repeat" && op !== "project.reschedule-repeat")
176
+ return true;
177
+ return !isRepeatReanchor(params);
178
+ }
157
179
  //# sourceMappingURL=operations.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"operations.js","sourceRoot":"","sources":["../../src/write/operations.ts"],"names":[],"mappings":"AAOA,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,UAAU;IACV,aAAa;IACb,eAAe;IACf,aAAa;IACb,aAAa;IACb,WAAW;IACX,eAAe;IACf,wBAAwB;IACxB,0BAA0B;IAC1B,aAAa;IACb,aAAa;IACb,gBAAgB;IAChB,kBAAkB;IAClB,gBAAgB;IAChB,UAAU;IACV,aAAa;IACb,SAAS;IACT,YAAY;IACZ,aAAa;IACb,SAAS;IACT,gBAAgB;IAChB,aAAa;IACb,YAAY;IACZ,cAAc;IACd,cAAc;IACd,mBAAmB;IACnB,gBAAgB;IAChB,gBAAgB;IAChB,iBAAiB;IACjB,kBAAkB;IAClB,gBAAgB;IAChB,mBAAmB;IACnB,qBAAqB;IACrB,wBAAwB;IACxB,yBAAyB;IACzB,2BAA2B;IAC3B,yBAAyB;IACzB,sBAAsB;IACtB,iCAAiC;IACjC,0BAA0B;IAC1B,2BAA2B;IAC3B,qBAAqB;IACrB,wBAAwB;IACxB,mBAAmB;IACnB,oBAAoB;IACpB,uBAAuB;IACvB,yBAAyB;IACzB,2BAA2B;IAC3B,sBAAsB;IACtB,uBAAuB;IACvB,cAAc;IACd,wBAAwB;IACxB,uBAAuB;IACvB,oBAAoB;IACpB,YAAY;IACZ,eAAe;IACf,SAAS;CACD,CAAC;AAIX;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,YAAY,GAA6B;IACpD,qBAAqB;IACrB,wBAAwB;IACxB,mBAAmB;IACnB,oBAAoB;IACpB,sEAAsE;IACtE,6EAA6E;IAC7E,+EAA+E;IAC/E,0EAA0E;IAC1E,6DAA6D;IAC7D,uBAAuB;IACvB,yBAAyB;IACzB,yBAAyB;IACzB,2BAA2B;IAC3B,sBAAsB;IACtB,uBAAuB;IACvB,cAAc;IACd,uEAAuE;IACvE,4EAA4E;IAC5E,6EAA6E;IAC7E,iEAAiE;IACjE,wBAAwB;IACxB,2EAA2E;IAC3E,4EAA4E;IAC5E,+EAA+E;IAC/E,kEAAkE;IAClE,iCAAiC;IACjC,6EAA6E;IAC7E,yEAAyE;IACzE,2EAA2E;IAC3E,8EAA8E;IAC9E,gBAAgB;IAChB,0BAA0B;IAC1B,4EAA4E;IAC5E,+EAA+E;IAC/E,8EAA8E;IAC9E,gFAAgF;IAChF,6EAA6E;IAC7E,+EAA+E;IAC/E,uEAAuE;IACvE,4DAA4D;IAC5D,sBAAsB;CACd,CAAC;AAEX,MAAM,UAAU,WAAW,CAAC,EAAiB;IAC3C,OAAO,YAAY,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;AACnC,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,WAAW,GAA6B;IACnD,qBAAqB;IACrB,wBAAwB;IACxB,yBAAyB;IACzB,2BAA2B;IAC3B,yBAAyB;IACzB,sBAAsB;IACtB,iCAAiC;IACjC,0BAA0B;CAClB,CAAC;AAEX,MAAM,UAAU,WAAW,CAAC,EAAiB;IAC3C,OAAO,WAAW,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;AAClC,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAA6B;IAC1D,wBAAwB;IACxB,yBAAyB;IACzB,2BAA2B;IAC3B,yBAAyB;IACzB,0BAA0B;CAClB,CAAC;AAEX,MAAM,UAAU,iBAAiB,CAAC,EAAiB;IACjD,OAAO,kBAAkB,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;AACzC,CAAC;AAyhBD,MAAM,CAAC,MAAM,QAAQ,GAAuB;IAC1C,QAAQ;IACR,QAAQ;IACR,SAAS;IACT,WAAW;IACX,UAAU;IACV,QAAQ;IACR,UAAU;CACF,CAAC"}
1
+ {"version":3,"file":"operations.js","sourceRoot":"","sources":["../../src/write/operations.ts"],"names":[],"mappings":"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"}
@@ -25,10 +25,34 @@
25
25
  * operation asserts a whole-database singleton, or nothing was recorded — the
26
26
  * resubmission is REFUSED with a pointer at the item, never guessed.
27
27
  *
28
- * Every other result class (blocked, unsupported, intent, the other
29
- * verify-failed reasons) is not a match: a blocked/unsupported attempt changed
30
- * nothing, a mismatch/silent-noop landed something the caller must look at
31
- * first, and an intent marker means the attempt may still be in flight.
28
+ * Every other FINAL result class (blocked, unsupported, the other verify-failed
29
+ * reasons) is not a match: a blocked/unsupported attempt changed nothing, and a
30
+ * mismatch/silent-noop landed something the caller must look at first.
31
+ *
32
+ * The third class is an UNSUPERSEDED INTENT — a keyed write that recorded its
33
+ * start and has written no final record yet (#639). That is not a fourth match
34
+ * rule but a different question, and it is answered by asking whether the
35
+ * process that wrote it is still alive:
36
+ *
37
+ * - HOLDER ALIVE — the original is still running. The resubmission is REFUSED
38
+ * (`blocked:in-flight`) and pointed at `things op-result <key>`. This is the
39
+ * refusal that closes the race the pre-lock lookback used to leave open: a
40
+ * retry fired while the original is mid-drive used to see no record, queue
41
+ * behind the mutation lock, and execute the whole verb a second time once the
42
+ * original released it.
43
+ * - HOLDER GONE — the writer died between touching the app and recording an
44
+ * outcome, which is the SAME ambiguity a `verify-failed:timeout` leaves, so it
45
+ * takes the same answer: the intent's own recorded assertion is the presence
46
+ * oracle, re-evaluated against current state. An intent that recorded no
47
+ * usable oracle refuses honestly rather than guessing.
48
+ *
49
+ * DOUBLE-CHECKED LOCKING. Every caller consults this gate TWICE: once before
50
+ * taking the mutation lock (the cheap fast path — most resubmissions are settled
51
+ * by then and never touch the lock) and again immediately after acquiring it,
52
+ * before the first mutating leg. The second check is the load-bearing one: a
53
+ * retry that passed the first check while the original was still running, then
54
+ * waited out the lock, finds the original's record on the way back in and
55
+ * replays instead of re-executing.
32
56
  */
33
57
  import type { AuditRecord } from "../audit/schema.ts";
34
58
  import { type MutationResult, type WriteDeps, type WriteOptions } from "./pipeline.ts";
@@ -42,7 +66,12 @@ export declare const OP_ID_RE: RegExp;
42
66
  */
43
67
  export declare const OPID_LOOKBACK_RECORDS = 1000;
44
68
  export declare const OPID_LOOKBACK_MS: number;
45
- /** The disclosure a reconciled replay carries — see {@link reconcileTimedOut}. */
69
+ /**
70
+ * The disclosure a reconciled replay carries — see {@link reconcileAmbiguous}.
71
+ * Deliberately worded for BOTH ambiguous origins (a confirmation that timed out,
72
+ * and a writer that died before recording an outcome): the caller's situation
73
+ * and next move are identical either way, and the record itself says which.
74
+ */
46
75
  export declare const RECONCILED_NOTE: string;
47
76
  /**
48
77
  * The recent-history lookback for an opId: the most recent MATCHABLE record
@@ -51,6 +80,26 @@ export declare const RECONCILED_NOTE: string;
51
80
  * blocked records are naturally excluded.
52
81
  */
53
82
  export declare function findOpIdRecord(records: AuditRecord[], opId: string, now: Date): AuditRecord | undefined;
83
+ /**
84
+ * The UNSUPERSEDED intent for a key, if there is one: an `intent` record whose
85
+ * ATTEMPT never recorded an outcome.
86
+ *
87
+ * Supersession pairs an intent to its own final by `ts`, which is the schema's
88
+ * documented sibling invariant — both records of one attempt derive from the
89
+ * same `startedAt`, so they carry the same timestamp (`audit/schema.ts`). It is
90
+ * deliberately NOT "the last record wins": `readAuditRecords` RE-SORTS the trail
91
+ * by `ts`, so file order does not survive the read, and a composite's intent
92
+ * (written when it takes the lock) briefly carried a later `ts` than its own
93
+ * summary (stamped when the verb began) — which sorted the intent last and left
94
+ * every finished promote looking permanently in flight. Measured in TORPH1
95
+ * cell B; the fix is this pairing plus stamping a composite's intent with the
96
+ * verb's own `startedAt`.
97
+ *
98
+ * Pairing also handles the RE-DISPATCH shape by construction: a key retried
99
+ * after an ambiguous failure records intent+final for the first attempt and a
100
+ * fresh intent for the second, and only the second is unpaired.
101
+ */
102
+ export declare function findPendingIntent(records: AuditRecord[], opId: string, now: Date): AuditRecord | undefined;
54
103
  /**
55
104
  * Can the recorded assertion decide, after the fact, whether the timed-out
56
105
  * change landed? Usable means the assertion is keyed to a SUBJECT the attempt
@@ -69,9 +118,15 @@ export type PresenceOracle = {
69
118
  export declare function presenceOracle(spec: DeltaSpec | undefined): PresenceOracle;
70
119
  /**
71
120
  * The lookback as a DISPATCH DECISION over an already-read trail: the replay
72
- * result when this key was already applied, a refusal when it matched an
73
- * unsettleable attempt, or null when the write must run. Shared by the batch
74
- * line path and {@link replayIfApplied}.
121
+ * result when this key was already applied, a refusal when it is still running
122
+ * or matched an unsettleable attempt, or null when the write must run. Shared by
123
+ * the batch line path and {@link replayIfApplied}.
124
+ *
125
+ * An unsuperseded intent is consulted FIRST, because it describes the key's
126
+ * present, while any matchable final describes its past. (Both can exist: a key
127
+ * whose first attempt timed out and is now being re-driven has an old timeout
128
+ * final AND a live intent. Acting on the final there would replay a change the
129
+ * running attempt has not finished making.)
75
130
  */
76
131
  export declare function resolveOpId(deps: WriteDeps, records: AuditRecord[], opId: string, now: Date): MutationResult | null;
77
132
  /**
@@ -82,6 +137,12 @@ export declare function resolveOpId(deps: WriteDeps, records: AuditRecord[], opI
82
137
  * flip-dance, and the promote-via-clone verbs; they never reach `run`, so
83
138
  * without this the key would be recorded and never honored).
84
139
  *
140
+ * Called TWICE per keyed write, and the second call is the one that matters —
141
+ * see the module header on double-checked locking. Both calls re-read the trail:
142
+ * the whole point of the post-acquire check is that the trail changed while this
143
+ * process was waiting for the lock, so a cached read would answer the stale
144
+ * question all over again.
145
+ *
85
146
  * Three conditions fail-open (run the write) rather than fail-closed: no key, a
86
147
  * dry run (it mints and records nothing, so there is nothing to deduplicate
87
148
  * against), or no trail to read.
@@ -1,3 +1,5 @@
1
+ import { instanceAlive } from "../process-instance.js";
2
+ import { attach, disclose, disclosuresOf } from "./disclosures.js";
1
3
  import { replayResultFromRecord, } from "./pipeline.js";
2
4
  import { readAuditRecords } from "./undo.js";
3
5
  import { createDbReader, evaluateDelta } from "./verify/delta.js";
@@ -10,10 +12,15 @@ export const OP_ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
10
12
  */
11
13
  export const OPID_LOOKBACK_RECORDS = 1000;
12
14
  export const OPID_LOOKBACK_MS = 7 * 24 * 60 * 60 * 1000;
13
- /** The disclosure a reconciled replay carries — see {@link reconcileTimedOut}. */
14
- export const RECONCILED_NOTE = "an earlier submission with this idempotency key did not finish confirming; the change was " +
15
- "found already in place, so nothing ran again. It cannot be reversed with `things undo` — the " +
16
- "earlier attempt never recorded a completed change to reverse";
15
+ /**
16
+ * The disclosure a reconciled replay carries see {@link reconcileAmbiguous}.
17
+ * Deliberately worded for BOTH ambiguous origins (a confirmation that timed out,
18
+ * and a writer that died before recording an outcome): the caller's situation
19
+ * and next move are identical either way, and the record itself says which.
20
+ */
21
+ export const RECONCILED_NOTE = "an earlier submission with this idempotency key never recorded a completed change; the change " +
22
+ "was found already in place, so nothing ran again. It cannot be reversed with `things undo` — " +
23
+ "there is no completed-change record to reverse";
17
24
  /** Result classes an `opId` match acts on (see the module header). */
18
25
  function matchable(result) {
19
26
  return result === "ok" || result === "verify-failed:timeout";
@@ -37,6 +44,37 @@ export function findOpIdRecord(records, opId, now) {
37
44
  }
38
45
  return match;
39
46
  }
47
+ /**
48
+ * The UNSUPERSEDED intent for a key, if there is one: an `intent` record whose
49
+ * ATTEMPT never recorded an outcome.
50
+ *
51
+ * Supersession pairs an intent to its own final by `ts`, which is the schema's
52
+ * documented sibling invariant — both records of one attempt derive from the
53
+ * same `startedAt`, so they carry the same timestamp (`audit/schema.ts`). It is
54
+ * deliberately NOT "the last record wins": `readAuditRecords` RE-SORTS the trail
55
+ * by `ts`, so file order does not survive the read, and a composite's intent
56
+ * (written when it takes the lock) briefly carried a later `ts` than its own
57
+ * summary (stamped when the verb began) — which sorted the intent last and left
58
+ * every finished promote looking permanently in flight. Measured in TORPH1
59
+ * cell B; the fix is this pairing plus stamping a composite's intent with the
60
+ * verb's own `startedAt`.
61
+ *
62
+ * Pairing also handles the RE-DISPATCH shape by construction: a key retried
63
+ * after an ambiguous failure records intent+final for the first attempt and a
64
+ * fresh intent for the second, and only the second is unpaired.
65
+ */
66
+ export function findPendingIntent(records, opId, now) {
67
+ const cutoff = now.getTime() - OPID_LOOKBACK_MS;
68
+ const window = records.slice(-OPID_LOOKBACK_RECORDS);
69
+ const inWindow = (r) => r.opId === opId && new Date(r.ts).getTime() >= cutoff;
70
+ const settled = new Set();
71
+ for (const r of window) {
72
+ if (inWindow(r) && r.result !== "intent")
73
+ settled.add(r.ts); // that attempt finished
74
+ }
75
+ // The newest unpaired intent (there is at most one in practice).
76
+ return window.findLast((r) => inWindow(r) && r.result === "intent" && !settled.has(r.ts));
77
+ }
40
78
  export function presenceOracle(spec) {
41
79
  if (spec === undefined) {
42
80
  return {
@@ -94,30 +132,62 @@ export function presenceOracle(spec) {
94
132
  /** The refusal: the key matched an unconfirmed attempt nothing here can settle. */
95
133
  function unreconcilable(record, why) {
96
134
  const target = record.uuid === null ? "the item" : record.uuid;
135
+ // An intent's writer DIED; a timeout's writer finished and could not confirm.
136
+ // Both leave the same ambiguity, and the caller acts on it the same way, but
137
+ // saying which one happened is what makes the trace worth looking at.
138
+ const how = record.result === "intent"
139
+ ? `started a change and its process ended without recording an outcome`
140
+ : `never confirmed its change`;
97
141
  return {
98
142
  kind: "blocked",
99
143
  op: record.op,
100
144
  reason: "reconcile",
101
- detail: `an earlier submission with this idempotency key (${record.ts}) never confirmed its ` +
102
- `change, and ${why} — so whether that change took effect cannot be settled from here`,
145
+ detail: `an earlier submission with this idempotency key (${record.ts}) ${how}, and ${why} — so ` +
146
+ "whether that change took effect cannot be settled from here",
103
147
  remediation: `check ${target} in Things (\`things op-result ${record.opId ?? "<key>"}\` reports what the ` +
104
148
  "earlier attempt recorded); once you know whether the change is there, leave it alone or " +
105
149
  "re-run with a NEW --op-id",
106
150
  };
107
151
  }
108
152
  /**
109
- * Reconcile a resubmission against a TIMED-OUT original: re-read current state
110
- * through the attempt's own recorded assertion and decide.
153
+ * The refusal: this key is STILL RUNNING somewhere. Returned instead of letting
154
+ * the resubmission queue behind the mutation lock, because waiting there is how
155
+ * the double happened — the waiter wakes up after the original finishes and, on
156
+ * the pre-lock answer it is still carrying, executes the verb again.
157
+ *
158
+ * Refusing costs the caller one poll. It is also the only answer that is true
159
+ * for the whole class: nothing here can know whether the running original will
160
+ * succeed, so nothing here should act on a guess about it.
161
+ */
162
+ function stillInFlight(record, holderPid) {
163
+ return {
164
+ kind: "blocked",
165
+ op: record.op,
166
+ reason: "in-flight",
167
+ detail: `an earlier submission with this idempotency key is STILL RUNNING (started ${record.ts}, ` +
168
+ `pid ${holderPid}) — it has not recorded an outcome yet, so nothing was run again here`,
169
+ remediation: `poll \`things op-result ${record.opId ?? "<key>"}\` until it reports a final outcome, then ` +
170
+ "act on that; do not resubmit while it is running (a GUI-driven change takes seconds, and " +
171
+ "longer on a large or syncing database)",
172
+ };
173
+ }
174
+ /**
175
+ * Reconcile a resubmission against an AMBIGUOUS original — one that touched the
176
+ * app and never recorded whether the change landed. Two records reach here: a
177
+ * `verify-failed:timeout` final (the attempt finished and could not confirm) and
178
+ * an orphaned `intent` (the attempt's process died mid-flight, #639). The
179
+ * ambiguity is identical, so the answer is: re-read current state through the
180
+ * attempt's own recorded assertion and decide.
111
181
  *
112
182
  * - satisfied → the change is there: replay it as already-applied, disclosing
113
- * that a timeout was reconciled. The replay carries NO undo token — the trail
114
- * holds no completed-change record for undo to reverse — and takes its uuid
115
- * from the re-read, which is how a create whose uuid was never discovered
116
- * (recorded `uuid: null`) still answers with the row it made.
183
+ * the reconciliation. The replay carries NO undo token — the trail holds no
184
+ * completed-change record for undo to reverse — and takes its uuid from the
185
+ * re-read, which is how a create whose uuid was never discovered (recorded
186
+ * `uuid: null`) still answers with the row it made.
117
187
  * - not satisfied → the change is absent: return null so the write runs.
118
188
  * - no usable oracle → refuse (never a guess).
119
189
  */
120
- function reconcileTimedOut(deps, record) {
190
+ function reconcileAmbiguous(deps, record) {
121
191
  const oracle = presenceOracle(record.expected);
122
192
  if (!oracle.usable)
123
193
  return unreconcilable(record, oracle.why);
@@ -132,25 +202,45 @@ function reconcileTimedOut(deps, record) {
132
202
  if (replay.kind !== "ok")
133
203
  return replay; // replayResultFromRecord always builds an ok
134
204
  const { undoToken: _noToken, ...rest } = replay;
135
- return {
205
+ const bag = disclosuresOf(replay);
206
+ disclose(bag, "reconciled-replay", RECONCILED_NOTE);
207
+ return attach({
136
208
  ...rest,
137
209
  uuid: evaluation.discoveredUuid ?? record.uuid,
138
210
  observed: evaluation.observed,
139
211
  ...(evaluation.repeating !== undefined && { repeating: evaluation.repeating }),
140
- warnings: [...(replay.warnings ?? []), RECONCILED_NOTE],
141
- };
212
+ }, bag);
142
213
  }
143
214
  /**
144
215
  * The lookback as a DISPATCH DECISION over an already-read trail: the replay
145
- * result when this key was already applied, a refusal when it matched an
146
- * unsettleable attempt, or null when the write must run. Shared by the batch
147
- * line path and {@link replayIfApplied}.
216
+ * result when this key was already applied, a refusal when it is still running
217
+ * or matched an unsettleable attempt, or null when the write must run. Shared by
218
+ * the batch line path and {@link replayIfApplied}.
219
+ *
220
+ * An unsuperseded intent is consulted FIRST, because it describes the key's
221
+ * present, while any matchable final describes its past. (Both can exist: a key
222
+ * whose first attempt timed out and is now being re-driven has an old timeout
223
+ * final AND a live intent. Acting on the final there would replay a change the
224
+ * running attempt has not finished making.)
148
225
  */
149
226
  export function resolveOpId(deps, records, opId, now) {
227
+ const pending = findPendingIntent(records, opId, now);
228
+ if (pending !== undefined) {
229
+ const holder = pending.holder;
230
+ // No holder recorded (an M3 intent from before write-ahead intents, or a
231
+ // path that writes one unkeyed): liveness is unknowable, and the safe
232
+ // unknowable answer is the one that never doubles — treat it as running.
233
+ if (holder === undefined || instanceAlive(holder)) {
234
+ return stillInFlight(pending, holder?.pid ?? -1);
235
+ }
236
+ // The holder is gone and no outcome was ever recorded: same ambiguity as a
237
+ // timeout, same oracle-driven answer.
238
+ return reconcileAmbiguous(deps, pending);
239
+ }
150
240
  const match = findOpIdRecord(records, opId, now);
151
241
  if (match === undefined)
152
242
  return null;
153
- return match.result === "ok" ? replayResultFromRecord(match) : reconcileTimedOut(deps, match);
243
+ return match.result === "ok" ? replayResultFromRecord(match) : reconcileAmbiguous(deps, match);
154
244
  }
155
245
  /**
156
246
  * The dispatch decision for a SINGLE invocation, reading the trail itself. The
@@ -160,6 +250,12 @@ export function resolveOpId(deps, records, opId, now) {
160
250
  * flip-dance, and the promote-via-clone verbs; they never reach `run`, so
161
251
  * without this the key would be recorded and never honored).
162
252
  *
253
+ * Called TWICE per keyed write, and the second call is the one that matters —
254
+ * see the module header on double-checked locking. Both calls re-read the trail:
255
+ * the whole point of the post-acquire check is that the trail changed while this
256
+ * process was waiting for the lock, so a cached read would answer the stale
257
+ * question all over again.
258
+ *
163
259
  * Three conditions fail-open (run the write) rather than fail-closed: no key, a
164
260
  * dry run (it mints and records nothing, so there is nothing to deduplicate
165
261
  * against), or no trail to read.
@@ -1 +1 @@
1
- {"version":3,"file":"opid.js","sourceRoot":"","sources":["../../src/write/opid.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"opid.js","sourceRoot":"","sources":["../../src/write/opid.ts"],"names":[],"mappings":"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"}
@@ -523,6 +523,18 @@ const REPEAT_RULE = {
523
523
  startDaysEarlier: opt(int(0, 366, "a whole number of days ≥ 0")),
524
524
  next: opt(isoDate()),
525
525
  };
526
+ /**
527
+ * reschedule-repeat: the same vocabulary with `frequency` + `interval` OPTIONAL,
528
+ * because the verb has two spellings — restate the rule (both required), or move
529
+ * the next occurrence with `next` alone (the series RE-ANCHOR). Which one this is,
530
+ * and that a re-anchor carries nothing else, is settled by `assertRescheduleRule`
531
+ * — the schema's job is only to let the second shape through the type gate.
532
+ */
533
+ const RESCHEDULE_RULE = {
534
+ ...REPEAT_RULE,
535
+ frequency: opt(enumOf(["daily", "weekly", "monthly", "yearly"])),
536
+ interval: opt(int(1, 99, "a whole number 1–99")),
537
+ };
526
538
  const SET_DATES = {
527
539
  uuid: str("an item uuid"),
528
540
  completedAt: opt(timestamp()),
@@ -685,12 +697,12 @@ export const PARAM_SCHEMAS = {
685
697
  "project.dissolve-heading": { uuid: str("a heading uuid") },
686
698
  "todo.clear-dated-reminder": UUID_ONLY,
687
699
  "todo.make-repeating": REPEAT_RULE,
688
- "todo.reschedule-repeat": REPEAT_RULE,
700
+ "todo.reschedule-repeat": RESCHEDULE_RULE,
689
701
  "todo.pause-repeat": UUID_ONLY,
690
702
  "todo.resume-repeat": UUID_ONLY,
691
703
  "todo.create-next-copy": UUID_ONLY,
692
704
  "todo.convert-to-project": UUID_ONLY,
693
- "project.reschedule-repeat": REPEAT_RULE,
705
+ "project.reschedule-repeat": RESCHEDULE_RULE,
694
706
  "project.pause-repeat": { uuid: str("a project uuid or unique title") },
695
707
  "project.resume-repeat": { uuid: str("a project uuid or unique title") },
696
708
  "area.reorder": {