synartesis 0.4.1 → 0.5.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/CHANGELOG.md +175 -0
- package/dist/{chunk-FOA4UIDE.js → chunk-CACV6R74.js} +124 -37
- package/dist/cli.js +81 -24
- package/dist/proxy.js +75 -14
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,181 @@
|
|
|
2
2
|
|
|
3
3
|
What changed, and why it mattered. Dates are release dates.
|
|
4
4
|
|
|
5
|
+
## 0.5.0 — 2026-09-10
|
|
6
|
+
|
|
7
|
+
A safety release. Two more races of the same family as 0.4.2's, one structural
|
|
8
|
+
change so that family cannot be written again, the first proof that a shipped
|
|
9
|
+
policy actually restores anything, and durability by default.
|
|
10
|
+
|
|
11
|
+
Minor rather than patch: two behaviours change in ways you will notice.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- **The journal is durable by default.** It ran with `synchronous = NORMAL`, on
|
|
16
|
+
the reasoning that the tail of the write-ahead log holds the record of a call
|
|
17
|
+
and never the call itself. True, and the wrong way round: the call reached the
|
|
18
|
+
server or it did not regardless of this file, so losing the record means the
|
|
19
|
+
world changed and the journal does not know. Undo cannot reverse what it has
|
|
20
|
+
no record of, and `show --live` would report that nothing was recorded — the
|
|
21
|
+
reassuring answer, on no evidence.
|
|
22
|
+
|
|
23
|
+
Measured here on 2,000 inserts of a 2 kB payload: NORMAL 0.0246 ms per write,
|
|
24
|
+
FULL 0.0594. Durability costs **0.035 ms per action**, about eight per cent of
|
|
25
|
+
the proxy's own 0.43 ms overhead. `SYNARTESIS_SYNC=normal` restores the old
|
|
26
|
+
behaviour.
|
|
27
|
+
|
|
28
|
+
- **An interrupted inverse no longer resumes itself.** An action in
|
|
29
|
+
`rolling_back` was let through without a claim, on the reasoning that it must
|
|
30
|
+
be a crash. It is also what a live undo looks like between claiming an action
|
|
31
|
+
and finishing its inverse, and nothing here can tell those apart, so a second
|
|
32
|
+
undo starting in that window sent the same inverse again. It now halts and
|
|
33
|
+
names the evidence to look at. Finishing such an inverse needs a lease that
|
|
34
|
+
distinguishes a live owner from an abandoned attempt, which needs a schema
|
|
35
|
+
and a migration path this build does not have; that limit is now stated
|
|
36
|
+
rather than silently crossed.
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- **Adapter contract tests for the shipped filesystem policy**, run against the
|
|
41
|
+
real server. Policies were checked only for tool *existence* — that
|
|
42
|
+
`fs.read_text_file` is a real tool — and never for whether the declared
|
|
43
|
+
inverse puts the file back. That is the failure mode that looks fine: a real
|
|
44
|
+
tool, a clean resolution, a report saying `rolled_back`, and the file still
|
|
45
|
+
wrong. Drift detection cannot catch it, because nothing is wrong with the
|
|
46
|
+
drift.
|
|
47
|
+
|
|
48
|
+
The tests make the mutation, undo it and compare bytes: exact restoration
|
|
49
|
+
including trailing whitespace and non-ascii, repeated writes to one file, a
|
|
50
|
+
colleague's edit refused, absence told apart from a read that failed, and
|
|
51
|
+
inspection distinguishing a touched file from an untouched one. Changing the
|
|
52
|
+
inverse to restore `$.content` instead of `$snapshot.content` fails them.
|
|
53
|
+
|
|
54
|
+
The tested server version is named in the file. **memory, git and github ship
|
|
55
|
+
policies with no such proof**; those servers are not installed in this
|
|
56
|
+
repository and their guarantees remain assumed.
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
|
|
60
|
+
- **Every status change now goes through one door.** Three of thirteen
|
|
61
|
+
transitions were conditional; the other ten wrote whatever they were told and
|
|
62
|
+
were safe only by convention. The convention failed three times in two days.
|
|
63
|
+
There are now two ways to change a status and no third: one reports whether it
|
|
64
|
+
won the race, the other names the statuses the caller must be holding and
|
|
65
|
+
throws when the row is not in one of them. Turning it on found three states
|
|
66
|
+
nothing can produce, all of them in tests that had been setting end states
|
|
67
|
+
directly.
|
|
68
|
+
|
|
69
|
+
- **A second place one approval could authorise two calls.** The
|
|
70
|
+
missing-prior-state branch ignored the result of spending the approval.
|
|
71
|
+
|
|
72
|
+
## 0.4.3 — 2026-09-10
|
|
73
|
+
|
|
74
|
+
A correctness release. Seven reproduced defects in how effects are accounted
|
|
75
|
+
for and how recovery is planned. Each is pinned by a test that fails against
|
|
76
|
+
0.4.2 on real state and inverse-call counts, not on messages.
|
|
77
|
+
|
|
78
|
+
The theme is one mistake made in seven places: treating absence of evidence as
|
|
79
|
+
evidence. An error was read as proof nothing happened, a missing post-state as
|
|
80
|
+
permission to write anyway, a stripped rule as proof of absence, and an
|
|
81
|
+
unreadable resource as one with nothing left applied.
|
|
82
|
+
|
|
83
|
+
### Fixed
|
|
84
|
+
|
|
85
|
+
- **A timeout after the write was recorded as never applied.** The uncertainty
|
|
86
|
+
check knew only "Connection closed" and a downstream abort, so an upstream
|
|
87
|
+
that mutates a record and then times out fell through to `failed` — and
|
|
88
|
+
rollback steps over a failed action without looking at it. The change was
|
|
89
|
+
invisible to recovery.
|
|
90
|
+
|
|
91
|
+
- **`isError` was treated as proof of a refusal.** The protocol gives the flag
|
|
92
|
+
no transactional meaning: it covers business-logic failures that happen after
|
|
93
|
+
a write as readily as a refusal before one.
|
|
94
|
+
|
|
95
|
+
Both now ask instead of assuming. Where a pre-read was declared, that same
|
|
96
|
+
read says what is true now, and comparing it against what was there before
|
|
97
|
+
turns a guess into evidence: unchanged is recorded as never applied, changed
|
|
98
|
+
is recorded as applied so it can be undone, and a read that cannot answer
|
|
99
|
+
leaves the outcome unknown. Only evidence of no dispatch — no transport, or a
|
|
100
|
+
JSON-RPC rejection of the envelope — is now recorded as definitely not
|
|
101
|
+
applied. Where there is no pre-read to ask, an adapter can state the
|
|
102
|
+
guarantee itself with `refusal: clean` on a tool policy; the default is
|
|
103
|
+
`uncertain`.
|
|
104
|
+
|
|
105
|
+
- **Undo overwrote human edits when the post-state was missing.** A reversible
|
|
106
|
+
action promises evidence. Where the post-read failed at capture, rollback
|
|
107
|
+
reverted anyway, labelled it unverified and reported success — writing the
|
|
108
|
+
old value over whatever was there, with no `--force` required. It now halts
|
|
109
|
+
and says which half of the evidence is missing. `--force` is how a person
|
|
110
|
+
decides to proceed; the step stays unverified in the report. Compensable
|
|
111
|
+
actions, whose policies declare no pre-read at all, are deliberately not
|
|
112
|
+
caught by this.
|
|
113
|
+
|
|
114
|
+
- **`absentWhen` was stripped on the way back in.** Rollback and inspection
|
|
115
|
+
each parsed a stored verify read through a schema naming only server, tool
|
|
116
|
+
and args, so the absence rules were dropped and every read failure became
|
|
117
|
+
"the resource is gone". A "permission denied" was reported as a definite
|
|
118
|
+
change and could send an inverse. There is now one shared schema for a stored
|
|
119
|
+
read.
|
|
120
|
+
|
|
121
|
+
- **`--replan` could not recover a resolved conflict.** Taking the halt's own
|
|
122
|
+
advice — put the resource back, then replan — passed every check and then
|
|
123
|
+
failed to claim the row, reporting that another undo held it, which was never
|
|
124
|
+
true. Unresolved drift still halts.
|
|
125
|
+
|
|
126
|
+
- **Preview invented drift it would never meet.** Undo puts intervening states
|
|
127
|
+
back as it walks down; a preview sends nothing, so an older write to a
|
|
128
|
+
twice-written record was compared against the newest value and called drift.
|
|
129
|
+
The preview now carries the state each planned inverse would leave, and only
|
|
130
|
+
where that is knowable — a compensation stops the chain rather than guessing.
|
|
131
|
+
|
|
132
|
+
- **Inspection claimed nothing was left applied about resources it could not
|
|
133
|
+
read.** The summary counted only changed and unchanged, so an unreadable
|
|
134
|
+
resource fell through to the most reassuring sentence available. Unknowns are
|
|
135
|
+
now said first and never absorbed into a clean answer; `tally()` exposes the
|
|
136
|
+
counts. A failed post-read is also no longer reported as "no pre-read was
|
|
137
|
+
declared".
|
|
138
|
+
|
|
139
|
+
- **A second place where one approval could authorise two calls.** The
|
|
140
|
+
missing-prior-state branch ignored the boolean from `adoptApproval`, so a
|
|
141
|
+
caller that lost the claim proceeded anyway. It now asks, like the gate path.
|
|
142
|
+
|
|
143
|
+
## 0.4.2 — 2026-09-09
|
|
144
|
+
|
|
145
|
+
Three bugs, found by auditing the paths the last pass did not touch. Each is
|
|
146
|
+
pinned by a test that fails against 0.4.1.
|
|
147
|
+
|
|
148
|
+
### Fixed
|
|
149
|
+
|
|
150
|
+
- **One approval could authorise two irreversible calls.** Spending a standing
|
|
151
|
+
approval was an announcement rather than a claim: `markInFlight` and
|
|
152
|
+
`adoptApproval` both wrote unconditionally, so two proxies — which share one
|
|
153
|
+
journal, the reason `close` is never automatic — could read the same approved
|
|
154
|
+
row before either had used it, and both proceed. One person's yes, two emails
|
|
155
|
+
sent, which is the single thing this is here to prevent.
|
|
156
|
+
|
|
157
|
+
Both are now conditional on the row still being `approved` and report whether
|
|
158
|
+
they won it. `adoptApproval` spends first and only carries the approval across
|
|
159
|
+
if it did. The proxy treats losing the race as never having had an approval:
|
|
160
|
+
it asks. The guard already existed for inverses, and its comment describes
|
|
161
|
+
this exact failure; the approval path never got one.
|
|
162
|
+
|
|
163
|
+
- **`prune` deleted sessions still waiting on a person.** `--help` and the
|
|
164
|
+
README both promise that nothing waiting on a person is ever pruned. The query
|
|
165
|
+
enforced it for `pending`, `gated` and `rolling_back`, and not for the two
|
|
166
|
+
other statuses that mean the same thing: `approved`, somebody's yes the agent
|
|
167
|
+
has not spent, and `unrecoverable`, an undo that stopped because somebody had
|
|
168
|
+
changed the resource and is waiting for them to choose. Pruning the first threw
|
|
169
|
+
away a human decision; the second threw away both the conflict and the undo
|
|
170
|
+
they were deciding about.
|
|
171
|
+
|
|
172
|
+
- **Forcing an undo walked around the double-apply guard.** `markRollingBack`
|
|
173
|
+
claims an action by moving it out of `applied`, so two rollbacks cannot both
|
|
174
|
+
send one inverse. `undo --force`, added in 0.4.1, acts on rows an earlier
|
|
175
|
+
refusal left `unrecoverable` — which the claim did not know about, so it never
|
|
176
|
+
claimed them and never reported that it had not. Two concurrent forced undos
|
|
177
|
+
both sent the inverse: harmless for an idempotent write, a second real change
|
|
178
|
+
to the world for a compensable one.
|
|
179
|
+
|
|
5
180
|
## 0.4.1 — 2026-09-09
|
|
6
181
|
|
|
7
182
|
### Added
|
|
@@ -339,7 +339,9 @@ function openDatabase(path) {
|
|
|
339
339
|
db.pragma("journal_mode = WAL");
|
|
340
340
|
db.pragma("foreign_keys = ON");
|
|
341
341
|
db.pragma("busy_timeout = 5000");
|
|
342
|
-
db.pragma(
|
|
342
|
+
db.pragma(
|
|
343
|
+
process.env["SYNARTESIS_SYNC"]?.toLowerCase() === "normal" ? "synchronous = NORMAL" : "synchronous = FULL"
|
|
344
|
+
);
|
|
343
345
|
if (path !== ":memory:") {
|
|
344
346
|
restrictToOwner(path);
|
|
345
347
|
}
|
|
@@ -454,11 +456,46 @@ var SqliteJournal = class {
|
|
|
454
456
|
);
|
|
455
457
|
});
|
|
456
458
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
459
|
+
/**
|
|
460
|
+
* Every status change, in one place, and none of them silent.
|
|
461
|
+
*
|
|
462
|
+
* Three of thirteen transitions used to be conditional; the other ten wrote
|
|
463
|
+
* whatever they were told. That was safe only by convention -- each caller
|
|
464
|
+
* happened to hold a claim first -- and the convention failed three times:
|
|
465
|
+
* a forced undo, a spent approval, and an adopted one all wrote a status
|
|
466
|
+
* they did not own, and two irreversible calls went out on one person's yes.
|
|
467
|
+
*
|
|
468
|
+
* `from` is the set of statuses the caller must be holding. #settle throws
|
|
469
|
+
* when the row is not in one of them, because a caller that has lost its
|
|
470
|
+
* claim has lost the right to say what happened; #claim reports it instead,
|
|
471
|
+
* for the callers whose job is to race. Neither can be bypassed by writing
|
|
472
|
+
* status directly, because nothing else in this class does.
|
|
473
|
+
*/
|
|
474
|
+
#settle(operation, actionId, to, from, error) {
|
|
475
|
+
this.#run(operation, () => {
|
|
476
|
+
const slots = from.map(() => "?").join(",");
|
|
477
|
+
const sql = error === void 0 ? `UPDATE actions SET status = ? WHERE id = ? AND status IN (${slots})` : `UPDATE actions SET status = ?, error = ? WHERE id = ? AND status IN (${slots})`;
|
|
478
|
+
const args = error === void 0 ? [to, actionId, ...from] : [to, error, actionId, ...from];
|
|
479
|
+
const result = this.#db.prepare(sql).run(...args);
|
|
480
|
+
if (result.changes !== 1) {
|
|
481
|
+
const now = this.getAction(actionId)?.status ?? "gone";
|
|
482
|
+
throw new Error(
|
|
483
|
+
`cannot move ${actionId} to ${to}: it is ${now}, not one of ${from.join(", ")}`
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
#claim(operation, actionId, to, from, error) {
|
|
489
|
+
return this.#run(operation, () => {
|
|
490
|
+
const slots = from.map(() => "?").join(",");
|
|
491
|
+
const sql = error === void 0 ? `UPDATE actions SET status = ? WHERE id = ? AND status IN (${slots})` : `UPDATE actions SET status = ?, error = ? WHERE id = ? AND status IN (${slots})`;
|
|
492
|
+
const args = error === void 0 ? [to, actionId, ...from] : [to, error, actionId, ...from];
|
|
493
|
+
return this.#db.prepare(sql).run(...args).changes === 1;
|
|
460
494
|
});
|
|
461
495
|
}
|
|
496
|
+
markFailed(actionId, error) {
|
|
497
|
+
this.#settle("markFailed", actionId, "failed", ["pending", "gated", "approved"], error);
|
|
498
|
+
}
|
|
462
499
|
/**
|
|
463
500
|
* The call was interrupted, so whether the upstream applied it is genuinely
|
|
464
501
|
* unknown. The row deliberately stays `pending`: recording it as failed
|
|
@@ -466,20 +503,37 @@ var SqliteJournal = class {
|
|
|
466
503
|
* case surfaced rather than resolved by guesswork.
|
|
467
504
|
*/
|
|
468
505
|
markUnknown(actionId, error) {
|
|
469
|
-
this.#
|
|
470
|
-
this.#db.prepare("UPDATE actions SET status = 'pending', error = ? WHERE id = ?").run(error, actionId);
|
|
471
|
-
});
|
|
506
|
+
this.#settle("markUnknown", actionId, "pending", ["pending", "gated", "approved"], error);
|
|
472
507
|
}
|
|
473
|
-
|
|
508
|
+
/**
|
|
509
|
+
* Claim an action to send its inverse, or report that somebody else has.
|
|
510
|
+
*
|
|
511
|
+
* Conditional, so the transition is a claim rather than an announcement. Two
|
|
512
|
+
* rollbacks of one run both read the action as applied and both sent its
|
|
513
|
+
* inverse; for a compensating call rather than a restore, that is a second
|
|
514
|
+
* real change to the world.
|
|
515
|
+
*
|
|
516
|
+
* `from` is which statuses may be claimed. It exists because `undo --force`
|
|
517
|
+
* acts on rows left `unrecoverable` by an earlier refusal, and a claim that
|
|
518
|
+
* only knew `applied` never claimed those at all -- so the guard above was
|
|
519
|
+
* simply absent on the one path where a person had already been told the
|
|
520
|
+
* resource is contested.
|
|
521
|
+
*/
|
|
522
|
+
markRollingBack(actionId, from = ["applied"]) {
|
|
474
523
|
return this.#run("markRollingBack", () => {
|
|
475
|
-
const
|
|
524
|
+
const slots = from.map(() => "?").join(",");
|
|
525
|
+
const result = this.#db.prepare(
|
|
526
|
+
`UPDATE actions SET status = 'rolling_back' WHERE id = ? AND status IN (${slots})`
|
|
527
|
+
).run(actionId, ...from);
|
|
476
528
|
return result.changes === 1;
|
|
477
529
|
});
|
|
478
530
|
}
|
|
479
531
|
markRolledBack(actionId) {
|
|
480
|
-
this.#
|
|
481
|
-
|
|
482
|
-
|
|
532
|
+
this.#settle("markRolledBack", actionId, "rolled_back", [
|
|
533
|
+
"rolling_back",
|
|
534
|
+
"applied",
|
|
535
|
+
"unrecoverable"
|
|
536
|
+
]);
|
|
483
537
|
}
|
|
484
538
|
/**
|
|
485
539
|
* The upstream processed the inverse and refused it, so nothing was applied
|
|
@@ -488,9 +542,7 @@ var SqliteJournal = class {
|
|
|
488
542
|
* was briefly unwell, and rollback is expected to be retried (D7).
|
|
489
543
|
*/
|
|
490
544
|
markInverseRejected(actionId, error) {
|
|
491
|
-
this.#
|
|
492
|
-
this.#db.prepare("UPDATE actions SET status = 'applied', error = ? WHERE id = ?").run(error, actionId);
|
|
493
|
-
});
|
|
545
|
+
this.#settle("markInverseRejected", actionId, "applied", ["rolling_back"], error);
|
|
494
546
|
}
|
|
495
547
|
/**
|
|
496
548
|
* The inverse may or may not have reached the upstream. The row stays in
|
|
@@ -498,18 +550,24 @@ var SqliteJournal = class {
|
|
|
498
550
|
* current state rather than assuming either way.
|
|
499
551
|
*/
|
|
500
552
|
markUnknownInverse(actionId, error) {
|
|
501
|
-
this.#
|
|
502
|
-
this.#db.prepare("UPDATE actions SET status = 'rolling_back', error = ? WHERE id = ?").run(error, actionId);
|
|
503
|
-
});
|
|
553
|
+
this.#settle("markUnknownInverse", actionId, "rolling_back", ["rolling_back"], error);
|
|
504
554
|
}
|
|
505
555
|
markGated(actionId, why) {
|
|
506
|
-
this.#
|
|
507
|
-
this.#db.prepare("UPDATE actions SET status = 'gated', error = ? WHERE id = ?").run(why ?? null, actionId);
|
|
508
|
-
});
|
|
556
|
+
this.#settle("markGated", actionId, "gated", ["pending", "gated"], why ?? null);
|
|
509
557
|
}
|
|
558
|
+
/**
|
|
559
|
+
* Spend a standing approval, or report that somebody else already has.
|
|
560
|
+
*
|
|
561
|
+
* Conditional for the same reason markRollingBack is. Several proxies share
|
|
562
|
+
* one journal, so two can read the same approved row before either has used
|
|
563
|
+
* it, and an unconditional write let both proceed -- one person's yes
|
|
564
|
+
* authorising two irreversible calls, which is the single thing this is here
|
|
565
|
+
* to prevent.
|
|
566
|
+
*/
|
|
510
567
|
markInFlight(actionId) {
|
|
511
|
-
this.#run("markInFlight", () => {
|
|
512
|
-
this.#db.prepare("UPDATE actions SET status = 'pending' WHERE id = ?").run(actionId);
|
|
568
|
+
return this.#run("markInFlight", () => {
|
|
569
|
+
const result = this.#db.prepare("UPDATE actions SET status = 'pending' WHERE id = ? AND status = 'approved'").run(actionId);
|
|
570
|
+
return result.changes === 1;
|
|
513
571
|
});
|
|
514
572
|
}
|
|
515
573
|
/**
|
|
@@ -542,12 +600,18 @@ var SqliteJournal = class {
|
|
|
542
600
|
});
|
|
543
601
|
}
|
|
544
602
|
adoptApproval(actionId, granted) {
|
|
545
|
-
this.#run("adoptApproval", () => {
|
|
603
|
+
return this.#run("adoptApproval", () => {
|
|
546
604
|
const move = this.#db.transaction(() => {
|
|
605
|
+
const spent = this.#db.prepare(
|
|
606
|
+
"UPDATE actions SET status = 'denied', error = ? WHERE id = ? AND status = 'approved'"
|
|
607
|
+
).run(`${SPENT_APPROVAL} ${actionId}`, granted.id);
|
|
608
|
+
if (spent.changes !== 1) {
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
547
611
|
this.#db.prepare("UPDATE actions SET approved_by = ?, approved_at = ? WHERE id = ?").run(granted.approvedBy ?? null, granted.approvedAt ?? null, actionId);
|
|
548
|
-
|
|
612
|
+
return true;
|
|
549
613
|
});
|
|
550
|
-
move.immediate();
|
|
614
|
+
return move.immediate();
|
|
551
615
|
});
|
|
552
616
|
}
|
|
553
617
|
listGated() {
|
|
@@ -587,9 +651,11 @@ var SqliteJournal = class {
|
|
|
587
651
|
});
|
|
588
652
|
}
|
|
589
653
|
markUnrecoverable(actionId, error) {
|
|
590
|
-
this.#
|
|
591
|
-
|
|
592
|
-
|
|
654
|
+
this.#settle("markUnrecoverable", actionId, "unrecoverable", [
|
|
655
|
+
"applied",
|
|
656
|
+
"rolling_back",
|
|
657
|
+
"unrecoverable"
|
|
658
|
+
], error);
|
|
593
659
|
}
|
|
594
660
|
listRuns() {
|
|
595
661
|
return this.#run(
|
|
@@ -639,10 +705,18 @@ var SqliteJournal = class {
|
|
|
639
705
|
FROM runs r
|
|
640
706
|
WHERE r.status != 'active'
|
|
641
707
|
AND COALESCE(r.ended_at, r.started_at) < ?
|
|
708
|
+
-- Everything a person still has business with. approved is
|
|
709
|
+
-- somebody's yes that the agent has not spent yet, and
|
|
710
|
+
-- unrecoverable is an undo that stopped because somebody had
|
|
711
|
+
-- changed the resource, waiting for them to choose. Both were
|
|
712
|
+
-- missing, so the promise made in --help and the README -- that
|
|
713
|
+
-- nothing waiting on a person is ever pruned -- was not one this
|
|
714
|
+
-- query kept.
|
|
642
715
|
AND NOT EXISTS (
|
|
643
716
|
SELECT 1 FROM actions a
|
|
644
717
|
WHERE a.run_id = r.id
|
|
645
|
-
AND a.status IN
|
|
718
|
+
AND a.status IN
|
|
719
|
+
('pending','gated','approved','rolling_back','unrecoverable'))
|
|
646
720
|
ORDER BY at, r.rowid`
|
|
647
721
|
).all(before).map(
|
|
648
722
|
(row) => z.object({
|
|
@@ -880,6 +954,7 @@ var toolPolicy = z2.strictObject({
|
|
|
880
954
|
match: z2.string().min(1),
|
|
881
955
|
class: z2.enum(["readonly", "reversible", "compensable", "irreversible"]),
|
|
882
956
|
gate: z2.enum(["always", "on_write", "never"]).optional(),
|
|
957
|
+
refusal: z2.enum(["uncertain", "clean"]).optional(),
|
|
883
958
|
snapshot: callTemplate.optional(),
|
|
884
959
|
inverse: callTemplate.optional()
|
|
885
960
|
});
|
|
@@ -1036,6 +1111,7 @@ function withGate(policy) {
|
|
|
1036
1111
|
match: policy.match,
|
|
1037
1112
|
class: policy.class,
|
|
1038
1113
|
gate,
|
|
1114
|
+
refusal: policy.refusal ?? "uncertain",
|
|
1039
1115
|
...policy.snapshot === void 0 ? {} : { snapshot: toCall(policy.snapshot) },
|
|
1040
1116
|
...policy.inverse === void 0 ? {} : { inverse: toCall(policy.inverse) }
|
|
1041
1117
|
};
|
|
@@ -1306,7 +1382,7 @@ function literalLength(pattern) {
|
|
|
1306
1382
|
return pattern.length - pattern.split("*").length + 1;
|
|
1307
1383
|
}
|
|
1308
1384
|
function failClosed(qualifiedName) {
|
|
1309
|
-
return { match: qualifiedName, class: "irreversible", gate: "always" };
|
|
1385
|
+
return { match: qualifiedName, class: "irreversible", gate: "always", refusal: "uncertain" };
|
|
1310
1386
|
}
|
|
1311
1387
|
function createPolicyResolver(manifest) {
|
|
1312
1388
|
const compiled = manifest.tools.map((policy) => ({
|
|
@@ -1383,6 +1459,20 @@ function planInverse(call, context) {
|
|
|
1383
1459
|
}
|
|
1384
1460
|
return { server: target.server, tool: target.tool, args: resolveArgs(call, context) };
|
|
1385
1461
|
}
|
|
1462
|
+
var resolvedRead = z4.object({
|
|
1463
|
+
server: z4.string(),
|
|
1464
|
+
tool: z4.string(),
|
|
1465
|
+
args: z4.record(z4.string(), z4.unknown()),
|
|
1466
|
+
absentWhen: z4.array(z4.string()).optional()
|
|
1467
|
+
});
|
|
1468
|
+
function toResolvedRead(parsed) {
|
|
1469
|
+
return {
|
|
1470
|
+
server: parsed.server,
|
|
1471
|
+
tool: parsed.tool,
|
|
1472
|
+
args: parsed.args,
|
|
1473
|
+
...parsed.absentWhen === void 0 ? {} : { absentWhen: parsed.absentWhen }
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1386
1476
|
function planRead(call, context) {
|
|
1387
1477
|
const target = splitQualified(call.tool);
|
|
1388
1478
|
if (target === void 0) {
|
|
@@ -1403,10 +1493,6 @@ function isDisconnected(error) {
|
|
|
1403
1493
|
const message = error instanceof Error ? error.message : String(error);
|
|
1404
1494
|
return message.includes("Not connected") || message.includes("Connection closed");
|
|
1405
1495
|
}
|
|
1406
|
-
function mayHaveArrived(error) {
|
|
1407
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1408
|
-
return message.includes("Connection closed");
|
|
1409
|
-
}
|
|
1410
1496
|
async function runRead(router, read2, signal) {
|
|
1411
1497
|
const label = `${read2.server}.${read2.tool}`;
|
|
1412
1498
|
const upstream = router.byName(read2.server);
|
|
@@ -1486,11 +1572,12 @@ export {
|
|
|
1486
1572
|
refusal,
|
|
1487
1573
|
toPayload,
|
|
1488
1574
|
planInverse,
|
|
1575
|
+
resolvedRead,
|
|
1576
|
+
toResolvedRead,
|
|
1489
1577
|
planRead,
|
|
1490
1578
|
isDisconnected,
|
|
1491
|
-
mayHaveArrived,
|
|
1492
1579
|
runRead,
|
|
1493
1580
|
observeState,
|
|
1494
1581
|
connectStdioUpstream
|
|
1495
1582
|
};
|
|
1496
|
-
//# sourceMappingURL=chunk-
|
|
1583
|
+
//# sourceMappingURL=chunk-CACV6R74.js.map
|
package/dist/cli.js
CHANGED
|
@@ -20,12 +20,14 @@ import {
|
|
|
20
20
|
planRead,
|
|
21
21
|
proxyCommand,
|
|
22
22
|
qualify,
|
|
23
|
+
resolvedRead,
|
|
23
24
|
rule,
|
|
24
25
|
style,
|
|
25
26
|
toPayload,
|
|
27
|
+
toResolvedRead,
|
|
26
28
|
verifyAgainstServers,
|
|
27
29
|
wasRefused
|
|
28
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-CACV6R74.js";
|
|
29
31
|
import {
|
|
30
32
|
DriftConflict,
|
|
31
33
|
ManifestError,
|
|
@@ -384,6 +386,7 @@ async function rollback(options) {
|
|
|
384
386
|
);
|
|
385
387
|
let halted;
|
|
386
388
|
let leftInPlace = false;
|
|
389
|
+
const projected = /* @__PURE__ */ new Map();
|
|
387
390
|
for (const action of inScope) {
|
|
388
391
|
let forcedOver;
|
|
389
392
|
const early = classify(action, policies !== void 0, force || dryRun);
|
|
@@ -414,13 +417,14 @@ ${seen}` : seen;
|
|
|
414
417
|
continue;
|
|
415
418
|
}
|
|
416
419
|
const plan = parsedPlan.data;
|
|
417
|
-
const verifyRead =
|
|
420
|
+
const verifyRead = resolvedRead.safeParse(rebuilt.verify ?? action.verify);
|
|
418
421
|
const recordedPost = observation.safeParse(action.postSnapshot);
|
|
419
422
|
let verified = false;
|
|
420
423
|
if (recordedPost.success && verifyRead.success) {
|
|
424
|
+
const foreseen = dryRun ? projected.get(resourceKey(toResolvedRead(verifyRead.data))) : void 0;
|
|
421
425
|
let current;
|
|
422
426
|
try {
|
|
423
|
-
current = await observeState(router, verifyRead.data, signal);
|
|
427
|
+
current = foreseen !== void 0 && foreseen !== UNFORESEEABLE ? foreseen : await observeState(router, toResolvedRead(verifyRead.data), signal);
|
|
424
428
|
} catch (error) {
|
|
425
429
|
const reason = `could not read current state to check for drift: ${describe(error)}`;
|
|
426
430
|
halted = { seq: action.seq, reason, detail: "" };
|
|
@@ -471,6 +475,17 @@ ${seen}` : seen;
|
|
|
471
475
|
forcedOver = "the resource had changed since; that change was overwritten";
|
|
472
476
|
}
|
|
473
477
|
}
|
|
478
|
+
if (!verified && action.class === "reversible" && !force) {
|
|
479
|
+
const reason = unverifiedBecause(action);
|
|
480
|
+
halted = {
|
|
481
|
+
seq: action.seq,
|
|
482
|
+
reason,
|
|
483
|
+
detail: "Without it there is no way to tell this resource from one somebody has edited since, so the recorded value was not written.",
|
|
484
|
+
conflict: true
|
|
485
|
+
};
|
|
486
|
+
steps.push({ ...describeStep(action), kind: "halt", reason, verified: false, plan });
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
474
489
|
if (!verified && action.status === "rolling_back") {
|
|
475
490
|
const reason = "an inverse was already sent before an interruption and no pre-read is declared, so whether it applied cannot be determined";
|
|
476
491
|
halted = { seq: action.seq, reason, detail: action.error ?? "" };
|
|
@@ -480,6 +495,10 @@ ${seen}` : seen;
|
|
|
480
495
|
}
|
|
481
496
|
break;
|
|
482
497
|
}
|
|
498
|
+
if (dryRun && verifyRead.success) {
|
|
499
|
+
const after = intendedAfterInverse(action);
|
|
500
|
+
projected.set(resourceKey(toResolvedRead(verifyRead.data)), after ?? UNFORESEEABLE);
|
|
501
|
+
}
|
|
483
502
|
steps.push({
|
|
484
503
|
...describeStep(action),
|
|
485
504
|
kind: "revert",
|
|
@@ -491,10 +510,15 @@ ${seen}` : seen;
|
|
|
491
510
|
if (dryRun) {
|
|
492
511
|
continue;
|
|
493
512
|
}
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
513
|
+
const claimable = force || policies !== void 0 ? ["applied", "unrecoverable"] : ["applied"];
|
|
514
|
+
const claimed = journal.markRollingBack(action.id, claimable);
|
|
515
|
+
if (!claimed) {
|
|
516
|
+
const reason = action.status === "rolling_back" ? "an inverse for this action was already sent and never finished; whether another undo still holds it cannot be told from here" : "another undo is already working on this action";
|
|
517
|
+
halted = {
|
|
518
|
+
seq: action.seq,
|
|
519
|
+
reason,
|
|
520
|
+
detail: action.status === "rolling_back" ? "Nothing here can tell a live owner from a dead one, and this build has no lease to consult. Read the action with `show --live`: if the resource still shows the agent's write, the inverse never landed." : ""
|
|
521
|
+
};
|
|
498
522
|
steps[steps.length - 1] = {
|
|
499
523
|
...describeStep(action),
|
|
500
524
|
kind: "halt",
|
|
@@ -552,6 +576,10 @@ function unverifiedBecause(action) {
|
|
|
552
576
|
function describeStep(action) {
|
|
553
577
|
return { seq: action.seq, server: action.server, tool: action.tool };
|
|
554
578
|
}
|
|
579
|
+
function resourceKey(read) {
|
|
580
|
+
return canonical({ server: read.server, tool: read.tool, args: read.args });
|
|
581
|
+
}
|
|
582
|
+
var UNFORESEEABLE = /* @__PURE__ */ Symbol("unforeseeable");
|
|
555
583
|
function overwriteText(current, action) {
|
|
556
584
|
const intended2 = intendedAfterInverse(action);
|
|
557
585
|
if (intended2 === void 0) {
|
|
@@ -597,11 +625,6 @@ async function executeInverse(router, plan, idempotencyKey, signal) {
|
|
|
597
625
|
|
|
598
626
|
// src/rollback/inspect.ts
|
|
599
627
|
import { z as z3 } from "zod";
|
|
600
|
-
var inversePlan2 = z3.object({
|
|
601
|
-
server: z3.string(),
|
|
602
|
-
tool: z3.string(),
|
|
603
|
-
args: z3.record(z3.string(), z3.unknown())
|
|
604
|
-
});
|
|
605
628
|
var observation2 = z3.union([
|
|
606
629
|
z3.object({ present: z3.literal(true), value: z3.unknown() }),
|
|
607
630
|
z3.object({ present: z3.literal(false) })
|
|
@@ -612,7 +635,7 @@ function sameState2(a, b) {
|
|
|
612
635
|
function intended(action) {
|
|
613
636
|
return action.snapshot === void 0 ? void 0 : { present: true, value: action.snapshot };
|
|
614
637
|
}
|
|
615
|
-
function
|
|
638
|
+
function resourceKey2(plan) {
|
|
616
639
|
return canonical({ server: plan.server, tool: plan.tool, args: plan.args });
|
|
617
640
|
}
|
|
618
641
|
var SETTLED = /* @__PURE__ */ new Set(["failed", "denied", "gated", "approved"]);
|
|
@@ -635,17 +658,18 @@ async function inspect(options) {
|
|
|
635
658
|
resources.push({ ...at, condition: "restored" });
|
|
636
659
|
continue;
|
|
637
660
|
}
|
|
638
|
-
const verify =
|
|
661
|
+
const verify = resolvedRead.safeParse(action.verify);
|
|
639
662
|
const post = observation2.safeParse(action.postSnapshot);
|
|
640
663
|
if (!verify.success || !post.success) {
|
|
664
|
+
const note = !verify.success ? action.verify === void 0 ? "no pre-read was declared, so there is nothing to compare against" : "the recorded pre-read could not be read back" : action.postSnapshot === void 0 ? "the post-state was never captured, so there is nothing to compare against" : "the recorded post-state could not be read back";
|
|
641
665
|
resources.push({
|
|
642
666
|
...at,
|
|
643
667
|
condition: "unknowable",
|
|
644
|
-
note: action.inverse === void 0 ?
|
|
668
|
+
note: action.inverse === void 0 ? `${note}; and nothing was captured to restore` : note
|
|
645
669
|
});
|
|
646
670
|
continue;
|
|
647
671
|
}
|
|
648
|
-
const key =
|
|
672
|
+
const key = resourceKey2(toResolvedRead(verify.data));
|
|
649
673
|
if (seen.has(key)) {
|
|
650
674
|
resources.push({ ...at, condition: "superseded" });
|
|
651
675
|
continue;
|
|
@@ -653,7 +677,7 @@ async function inspect(options) {
|
|
|
653
677
|
seen.add(key);
|
|
654
678
|
let current;
|
|
655
679
|
try {
|
|
656
|
-
current = await observeState(router, verify.data, signal);
|
|
680
|
+
current = await observeState(router, toResolvedRead(verify.data), signal);
|
|
657
681
|
} catch (error) {
|
|
658
682
|
resources.push({ ...at, condition: "unknowable", note: `could not read it: ${describe(error)}` });
|
|
659
683
|
continue;
|
|
@@ -675,16 +699,49 @@ async function inspect(options) {
|
|
|
675
699
|
}
|
|
676
700
|
return { runId, resources: resources.reverse() };
|
|
677
701
|
}
|
|
702
|
+
function tally(inspection) {
|
|
703
|
+
const counts = {
|
|
704
|
+
unchanged: 0,
|
|
705
|
+
changed: 0,
|
|
706
|
+
restored: 0,
|
|
707
|
+
superseded: 0,
|
|
708
|
+
"not-applied": 0,
|
|
709
|
+
unknowable: 0
|
|
710
|
+
};
|
|
711
|
+
for (const resource of inspection.resources) {
|
|
712
|
+
counts[resource.condition] += 1;
|
|
713
|
+
}
|
|
714
|
+
return counts;
|
|
715
|
+
}
|
|
678
716
|
function verdict(inspection) {
|
|
679
|
-
const
|
|
680
|
-
const
|
|
681
|
-
if (changed > 0) {
|
|
682
|
-
|
|
717
|
+
const counts = tally(inspection);
|
|
718
|
+
const said = [];
|
|
719
|
+
if (counts.changed > 0) {
|
|
720
|
+
said.push(
|
|
721
|
+
`${String(counts.changed)} changed since this ran; undoing would write over ${counts.changed === 1 ? "it" : "them"}`
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
if (counts.unknowable > 0) {
|
|
725
|
+
said.push(
|
|
726
|
+
`${String(counts.unknowable)} could not be checked, so whether ${counts.unknowable === 1 ? "it is" : "they are"} still applied is unknown`
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
if (said.length > 0) {
|
|
730
|
+
if (counts.unchanged > 0) {
|
|
731
|
+
said.push(`${String(counts.unchanged)} unchanged`);
|
|
732
|
+
}
|
|
733
|
+
return said.join(" \xB7 ");
|
|
734
|
+
}
|
|
735
|
+
if (counts.unchanged > 0) {
|
|
736
|
+
return `nothing has been touched since; all ${String(counts.unchanged)} would undo cleanly`;
|
|
737
|
+
}
|
|
738
|
+
if (counts.restored > 0 || counts.superseded > 0) {
|
|
739
|
+
return "everything here has already been put back";
|
|
683
740
|
}
|
|
684
|
-
if (
|
|
685
|
-
return
|
|
741
|
+
if (counts["not-applied"] > 0) {
|
|
742
|
+
return "nothing here was ever applied";
|
|
686
743
|
}
|
|
687
|
-
return "nothing
|
|
744
|
+
return "nothing was recorded in this session";
|
|
688
745
|
}
|
|
689
746
|
|
|
690
747
|
// src/watch.ts
|
package/dist/proxy.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
canonical,
|
|
3
4
|
cliCommandFrom,
|
|
4
5
|
connectStdioUpstream,
|
|
5
6
|
createPolicyResolver,
|
|
@@ -9,7 +10,6 @@ import {
|
|
|
9
10
|
isDisconnected,
|
|
10
11
|
loadManifest,
|
|
11
12
|
mark,
|
|
12
|
-
mayHaveArrived,
|
|
13
13
|
observeState,
|
|
14
14
|
openJournal,
|
|
15
15
|
planInverse,
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
runRead,
|
|
20
20
|
toPayload,
|
|
21
21
|
verifyAgainstServers
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-CACV6R74.js";
|
|
23
23
|
import {
|
|
24
24
|
SnapshotError,
|
|
25
25
|
UpstreamError,
|
|
@@ -322,6 +322,25 @@ function identityFor(router) {
|
|
|
322
322
|
}
|
|
323
323
|
return { name: "synartesis", version: "0.0.0" };
|
|
324
324
|
}
|
|
325
|
+
async function whatHappened(router, verify, before, signal) {
|
|
326
|
+
if (verify === void 0) {
|
|
327
|
+
return "unknown";
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
330
|
+
const now = await observeState(router, verify, signal);
|
|
331
|
+
return canonical(now) === canonical(before) ? "none" : "applied";
|
|
332
|
+
} catch {
|
|
333
|
+
return "unknown";
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function neverDispatched(error) {
|
|
337
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
338
|
+
if (message.includes("Not connected")) {
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
const code = isRecord(error) ? error["code"] : void 0;
|
|
342
|
+
return code === ErrorCode.MethodNotFound || code === ErrorCode.InvalidParams || code === ErrorCode.InvalidRequest || code === ErrorCode.ParseError;
|
|
343
|
+
}
|
|
325
344
|
var SYNARTESIS_INSTRUCTIONS = [
|
|
326
345
|
"These tools are guarded by Synartesis, which records every change so it can be undone later.",
|
|
327
346
|
"",
|
|
@@ -588,10 +607,11 @@ function createProxyServer(options) {
|
|
|
588
607
|
seq: reusable.seq,
|
|
589
608
|
idempotencyKey: reusable.idempotencyKey
|
|
590
609
|
};
|
|
610
|
+
let spent = true;
|
|
591
611
|
if (inherited !== void 0) {
|
|
592
|
-
journal.adoptApproval(pending.actionId, inherited);
|
|
612
|
+
spent = journal.adoptApproval(pending.actionId, inherited);
|
|
593
613
|
} else if (granted !== void 0 && waiting === void 0) {
|
|
594
|
-
journal.markInFlight(granted.id);
|
|
614
|
+
spent = journal.markInFlight(granted.id);
|
|
595
615
|
}
|
|
596
616
|
if (granted !== void 0) {
|
|
597
617
|
log?.info(
|
|
@@ -654,15 +674,17 @@ function createProxyServer(options) {
|
|
|
654
674
|
}
|
|
655
675
|
};
|
|
656
676
|
const askedAlready = wantsGate;
|
|
657
|
-
if (wantsGate && granted === void 0) {
|
|
677
|
+
if (wantsGate && (granted === void 0 || !spent)) {
|
|
658
678
|
await decide("this action cannot be undone");
|
|
659
679
|
}
|
|
660
680
|
let snapshot;
|
|
661
681
|
let verify;
|
|
682
|
+
let probe;
|
|
662
683
|
let missingPriorState;
|
|
663
684
|
if (policy.snapshot !== void 0) {
|
|
664
685
|
try {
|
|
665
686
|
verify = planRead(policy.snapshot, { args });
|
|
687
|
+
probe = verify;
|
|
666
688
|
snapshot = await runRead(router, verify, extra.signal);
|
|
667
689
|
journal.attachSnapshot(pending.actionId, snapshot);
|
|
668
690
|
} catch (error) {
|
|
@@ -683,6 +705,7 @@ function createProxyServer(options) {
|
|
|
683
705
|
}
|
|
684
706
|
}
|
|
685
707
|
}
|
|
708
|
+
const priorState = probe === void 0 || missingPriorState !== void 0 ? { present: false } : { present: true, value: snapshot };
|
|
686
709
|
if (missingPriorState !== void 0 && !askedAlready) {
|
|
687
710
|
const standing = journal.findApproval({
|
|
688
711
|
server: route.upstream.name,
|
|
@@ -694,12 +717,15 @@ function createProxyServer(options) {
|
|
|
694
717
|
await decide(
|
|
695
718
|
`nothing was captured to restore, so this cannot be undone \u2014 the read said: ${missingPriorState}`
|
|
696
719
|
);
|
|
697
|
-
} else {
|
|
698
|
-
journal.adoptApproval(pending.actionId, standing);
|
|
720
|
+
} else if (journal.adoptApproval(pending.actionId, standing)) {
|
|
699
721
|
log?.info(
|
|
700
722
|
{ action: pending.actionId, by: standing.approvedBy, from: standing.runId },
|
|
701
723
|
"proceeding on a standing approval"
|
|
702
724
|
);
|
|
725
|
+
} else {
|
|
726
|
+
await decide(
|
|
727
|
+
`nothing was captured to restore, so this cannot be undone \u2014 the read said: ${missingPriorState}`
|
|
728
|
+
);
|
|
703
729
|
}
|
|
704
730
|
}
|
|
705
731
|
const forwarded = {
|
|
@@ -712,12 +738,27 @@ function createProxyServer(options) {
|
|
|
712
738
|
});
|
|
713
739
|
const refused = refusal(result);
|
|
714
740
|
if (refused !== void 0) {
|
|
715
|
-
|
|
716
|
-
|
|
741
|
+
const settled = policy.refusal === "clean" ? "none" : await whatHappened(router, probe, priorState, extra.signal);
|
|
742
|
+
if (settled !== "applied") {
|
|
743
|
+
const why = `the upstream refused the call: ${refused}`;
|
|
744
|
+
if (settled === "none") {
|
|
745
|
+
journal.markFailed(pending.actionId, why);
|
|
746
|
+
} else {
|
|
747
|
+
journal.markUnknown(
|
|
748
|
+
pending.actionId,
|
|
749
|
+
`${why} -- and whether anything changed could not be established`
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
log?.debug(
|
|
753
|
+
{ seq: pending.seq, tool: route.tool, reason: refused, settled },
|
|
754
|
+
"refused by the upstream"
|
|
755
|
+
);
|
|
756
|
+
return result;
|
|
757
|
+
}
|
|
758
|
+
log?.warn(
|
|
717
759
|
{ seq: pending.seq, tool: route.tool, reason: refused },
|
|
718
|
-
"
|
|
760
|
+
"the upstream reported an error after changing the resource"
|
|
719
761
|
);
|
|
720
|
-
return result;
|
|
721
762
|
}
|
|
722
763
|
const context = { args, snapshot, result: toPayload(result) };
|
|
723
764
|
const warnings = [];
|
|
@@ -759,10 +800,30 @@ function createProxyServer(options) {
|
|
|
759
800
|
return result;
|
|
760
801
|
} catch (error) {
|
|
761
802
|
const disconnected = isDisconnected(error);
|
|
762
|
-
if (
|
|
763
|
-
journal.markUnknown(pending.actionId, describe(error));
|
|
764
|
-
} else {
|
|
803
|
+
if (neverDispatched(error)) {
|
|
765
804
|
journal.markFailed(pending.actionId, describe(error));
|
|
805
|
+
} else {
|
|
806
|
+
const settled = await whatHappened(router, probe, priorState, extra.signal);
|
|
807
|
+
if (settled === "none") {
|
|
808
|
+
journal.markFailed(pending.actionId, describe(error));
|
|
809
|
+
} else if (settled === "unknown") {
|
|
810
|
+
journal.markUnknown(pending.actionId, describe(error));
|
|
811
|
+
} else {
|
|
812
|
+
let recovered;
|
|
813
|
+
if (policy.inverse !== void 0 && missingPriorState === void 0) {
|
|
814
|
+
try {
|
|
815
|
+
recovered = planInverse(policy.inverse, { args, snapshot, result: void 0 });
|
|
816
|
+
} catch {
|
|
817
|
+
recovered = void 0;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
journal.markApplied(pending.actionId, {
|
|
821
|
+
result: void 0,
|
|
822
|
+
...recovered === void 0 ? {} : { inverse: recovered },
|
|
823
|
+
...verify === void 0 ? {} : { verify },
|
|
824
|
+
warning: `the call applied but its answer never arrived: ${describe(error)}`
|
|
825
|
+
});
|
|
826
|
+
}
|
|
766
827
|
}
|
|
767
828
|
if (disconnected && route.upstream.reconnect !== void 0) {
|
|
768
829
|
await route.upstream.reconnect().catch(() => void 0);
|