synartesis 0.4.1 → 0.4.2
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 +37 -0
- package/dist/{chunk-FOA4UIDE.js → chunk-LAZLEAZW.js} +50 -9
- package/dist/cli.js +6 -3
- package/dist/proxy.js +5 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
What changed, and why it mattered. Dates are release dates.
|
|
4
4
|
|
|
5
|
+
## 0.4.2 — 2026-09-09
|
|
6
|
+
|
|
7
|
+
Three bugs, found by auditing the paths the last pass did not touch. Each is
|
|
8
|
+
pinned by a test that fails against 0.4.1.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **One approval could authorise two irreversible calls.** Spending a standing
|
|
13
|
+
approval was an announcement rather than a claim: `markInFlight` and
|
|
14
|
+
`adoptApproval` both wrote unconditionally, so two proxies — which share one
|
|
15
|
+
journal, the reason `close` is never automatic — could read the same approved
|
|
16
|
+
row before either had used it, and both proceed. One person's yes, two emails
|
|
17
|
+
sent, which is the single thing this is here to prevent.
|
|
18
|
+
|
|
19
|
+
Both are now conditional on the row still being `approved` and report whether
|
|
20
|
+
they won it. `adoptApproval` spends first and only carries the approval across
|
|
21
|
+
if it did. The proxy treats losing the race as never having had an approval:
|
|
22
|
+
it asks. The guard already existed for inverses, and its comment describes
|
|
23
|
+
this exact failure; the approval path never got one.
|
|
24
|
+
|
|
25
|
+
- **`prune` deleted sessions still waiting on a person.** `--help` and the
|
|
26
|
+
README both promise that nothing waiting on a person is ever pruned. The query
|
|
27
|
+
enforced it for `pending`, `gated` and `rolling_back`, and not for the two
|
|
28
|
+
other statuses that mean the same thing: `approved`, somebody's yes the agent
|
|
29
|
+
has not spent, and `unrecoverable`, an undo that stopped because somebody had
|
|
30
|
+
changed the resource and is waiting for them to choose. Pruning the first threw
|
|
31
|
+
away a human decision; the second threw away both the conflict and the undo
|
|
32
|
+
they were deciding about.
|
|
33
|
+
|
|
34
|
+
- **Forcing an undo walked around the double-apply guard.** `markRollingBack`
|
|
35
|
+
claims an action by moving it out of `applied`, so two rollbacks cannot both
|
|
36
|
+
send one inverse. `undo --force`, added in 0.4.1, acts on rows an earlier
|
|
37
|
+
refusal left `unrecoverable` — which the claim did not know about, so it never
|
|
38
|
+
claimed them and never reported that it had not. Two concurrent forced undos
|
|
39
|
+
both sent the inverse: harmless for an idempotent write, a second real change
|
|
40
|
+
to the world for a compensable one.
|
|
41
|
+
|
|
5
42
|
## 0.4.1 — 2026-09-09
|
|
6
43
|
|
|
7
44
|
### Added
|
|
@@ -470,9 +470,26 @@ var SqliteJournal = class {
|
|
|
470
470
|
this.#db.prepare("UPDATE actions SET status = 'pending', error = ? WHERE id = ?").run(error, actionId);
|
|
471
471
|
});
|
|
472
472
|
}
|
|
473
|
-
|
|
473
|
+
/**
|
|
474
|
+
* Claim an action to send its inverse, or report that somebody else has.
|
|
475
|
+
*
|
|
476
|
+
* Conditional, so the transition is a claim rather than an announcement. Two
|
|
477
|
+
* rollbacks of one run both read the action as applied and both sent its
|
|
478
|
+
* inverse; for a compensating call rather than a restore, that is a second
|
|
479
|
+
* real change to the world.
|
|
480
|
+
*
|
|
481
|
+
* `from` is which statuses may be claimed. It exists because `undo --force`
|
|
482
|
+
* acts on rows left `unrecoverable` by an earlier refusal, and a claim that
|
|
483
|
+
* only knew `applied` never claimed those at all -- so the guard above was
|
|
484
|
+
* simply absent on the one path where a person had already been told the
|
|
485
|
+
* resource is contested.
|
|
486
|
+
*/
|
|
487
|
+
markRollingBack(actionId, from = ["applied"]) {
|
|
474
488
|
return this.#run("markRollingBack", () => {
|
|
475
|
-
const
|
|
489
|
+
const slots = from.map(() => "?").join(",");
|
|
490
|
+
const result = this.#db.prepare(
|
|
491
|
+
`UPDATE actions SET status = 'rolling_back' WHERE id = ? AND status IN (${slots})`
|
|
492
|
+
).run(actionId, ...from);
|
|
476
493
|
return result.changes === 1;
|
|
477
494
|
});
|
|
478
495
|
}
|
|
@@ -507,9 +524,19 @@ var SqliteJournal = class {
|
|
|
507
524
|
this.#db.prepare("UPDATE actions SET status = 'gated', error = ? WHERE id = ?").run(why ?? null, actionId);
|
|
508
525
|
});
|
|
509
526
|
}
|
|
527
|
+
/**
|
|
528
|
+
* Spend a standing approval, or report that somebody else already has.
|
|
529
|
+
*
|
|
530
|
+
* Conditional for the same reason markRollingBack is. Several proxies share
|
|
531
|
+
* one journal, so two can read the same approved row before either has used
|
|
532
|
+
* it, and an unconditional write let both proceed -- one person's yes
|
|
533
|
+
* authorising two irreversible calls, which is the single thing this is here
|
|
534
|
+
* to prevent.
|
|
535
|
+
*/
|
|
510
536
|
markInFlight(actionId) {
|
|
511
|
-
this.#run("markInFlight", () => {
|
|
512
|
-
this.#db.prepare("UPDATE actions SET status = 'pending' WHERE id = ?").run(actionId);
|
|
537
|
+
return this.#run("markInFlight", () => {
|
|
538
|
+
const result = this.#db.prepare("UPDATE actions SET status = 'pending' WHERE id = ? AND status = 'approved'").run(actionId);
|
|
539
|
+
return result.changes === 1;
|
|
513
540
|
});
|
|
514
541
|
}
|
|
515
542
|
/**
|
|
@@ -542,12 +569,18 @@ var SqliteJournal = class {
|
|
|
542
569
|
});
|
|
543
570
|
}
|
|
544
571
|
adoptApproval(actionId, granted) {
|
|
545
|
-
this.#run("adoptApproval", () => {
|
|
572
|
+
return this.#run("adoptApproval", () => {
|
|
546
573
|
const move = this.#db.transaction(() => {
|
|
574
|
+
const spent = this.#db.prepare(
|
|
575
|
+
"UPDATE actions SET status = 'denied', error = ? WHERE id = ? AND status = 'approved'"
|
|
576
|
+
).run(`${SPENT_APPROVAL} ${actionId}`, granted.id);
|
|
577
|
+
if (spent.changes !== 1) {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
547
580
|
this.#db.prepare("UPDATE actions SET approved_by = ?, approved_at = ? WHERE id = ?").run(granted.approvedBy ?? null, granted.approvedAt ?? null, actionId);
|
|
548
|
-
|
|
581
|
+
return true;
|
|
549
582
|
});
|
|
550
|
-
move.immediate();
|
|
583
|
+
return move.immediate();
|
|
551
584
|
});
|
|
552
585
|
}
|
|
553
586
|
listGated() {
|
|
@@ -639,10 +672,18 @@ var SqliteJournal = class {
|
|
|
639
672
|
FROM runs r
|
|
640
673
|
WHERE r.status != 'active'
|
|
641
674
|
AND COALESCE(r.ended_at, r.started_at) < ?
|
|
675
|
+
-- Everything a person still has business with. approved is
|
|
676
|
+
-- somebody's yes that the agent has not spent yet, and
|
|
677
|
+
-- unrecoverable is an undo that stopped because somebody had
|
|
678
|
+
-- changed the resource, waiting for them to choose. Both were
|
|
679
|
+
-- missing, so the promise made in --help and the README -- that
|
|
680
|
+
-- nothing waiting on a person is ever pruned -- was not one this
|
|
681
|
+
-- query kept.
|
|
642
682
|
AND NOT EXISTS (
|
|
643
683
|
SELECT 1 FROM actions a
|
|
644
684
|
WHERE a.run_id = r.id
|
|
645
|
-
AND a.status IN
|
|
685
|
+
AND a.status IN
|
|
686
|
+
('pending','gated','approved','rolling_back','unrecoverable'))
|
|
646
687
|
ORDER BY at, r.rowid`
|
|
647
688
|
).all(before).map(
|
|
648
689
|
(row) => z.object({
|
|
@@ -1493,4 +1534,4 @@ export {
|
|
|
1493
1534
|
observeState,
|
|
1494
1535
|
connectStdioUpstream
|
|
1495
1536
|
};
|
|
1496
|
-
//# sourceMappingURL=chunk-
|
|
1537
|
+
//# sourceMappingURL=chunk-LAZLEAZW.js.map
|
package/dist/cli.js
CHANGED
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
toPayload,
|
|
26
26
|
verifyAgainstServers,
|
|
27
27
|
wasRefused
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-LAZLEAZW.js";
|
|
29
29
|
import {
|
|
30
30
|
DriftConflict,
|
|
31
31
|
ManifestError,
|
|
@@ -491,8 +491,11 @@ ${seen}` : seen;
|
|
|
491
491
|
if (dryRun) {
|
|
492
492
|
continue;
|
|
493
493
|
}
|
|
494
|
-
const claimed = journal.markRollingBack(
|
|
495
|
-
|
|
494
|
+
const claimed = journal.markRollingBack(
|
|
495
|
+
action.id,
|
|
496
|
+
force ? ["applied", "unrecoverable"] : ["applied"]
|
|
497
|
+
);
|
|
498
|
+
if (!claimed && action.status !== "rolling_back") {
|
|
496
499
|
const reason = "another undo is already working on this action";
|
|
497
500
|
halted = { seq: action.seq, reason, detail: "" };
|
|
498
501
|
steps[steps.length - 1] = {
|
package/dist/proxy.js
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
runRead,
|
|
20
20
|
toPayload,
|
|
21
21
|
verifyAgainstServers
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-LAZLEAZW.js";
|
|
23
23
|
import {
|
|
24
24
|
SnapshotError,
|
|
25
25
|
UpstreamError,
|
|
@@ -588,10 +588,11 @@ function createProxyServer(options) {
|
|
|
588
588
|
seq: reusable.seq,
|
|
589
589
|
idempotencyKey: reusable.idempotencyKey
|
|
590
590
|
};
|
|
591
|
+
let spent = true;
|
|
591
592
|
if (inherited !== void 0) {
|
|
592
|
-
journal.adoptApproval(pending.actionId, inherited);
|
|
593
|
+
spent = journal.adoptApproval(pending.actionId, inherited);
|
|
593
594
|
} else if (granted !== void 0 && waiting === void 0) {
|
|
594
|
-
journal.markInFlight(granted.id);
|
|
595
|
+
spent = journal.markInFlight(granted.id);
|
|
595
596
|
}
|
|
596
597
|
if (granted !== void 0) {
|
|
597
598
|
log?.info(
|
|
@@ -654,7 +655,7 @@ function createProxyServer(options) {
|
|
|
654
655
|
}
|
|
655
656
|
};
|
|
656
657
|
const askedAlready = wantsGate;
|
|
657
|
-
if (wantsGate && granted === void 0) {
|
|
658
|
+
if (wantsGate && (granted === void 0 || !spent)) {
|
|
658
659
|
await decide("this action cannot be undone");
|
|
659
660
|
}
|
|
660
661
|
let snapshot;
|