@pify/swarm 0.9.0 → 0.9.1

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.
@@ -32,8 +32,8 @@ import {
32
32
  decideConsent,
33
33
  envConsent,
34
34
  parseConsent,
35
+ persistConsent,
35
36
  readConsent,
36
- writeConsent,
37
37
  } from "../src/consent.ts";
38
38
  import { LiveChildren, cancelNote, type CancelReason } from "../src/cancel.ts";
39
39
  import { DELIVERY_TYPE, deliveryMessage, pendingResult } from "../src/pending.ts";
@@ -630,8 +630,7 @@ export default function swarm(pi: ExtensionAPI) {
630
630
  ),
631
631
  );
632
632
  try {
633
- writeFileSync(file, `${JSON.stringify(writeConsent(store, ctx.cwd, "agents", approved), null, 2)}
634
- `);
633
+ persistConsent(file, ctx.cwd, "agents", approved);
635
634
  } catch {
636
635
  // An unwritable consent file costs us the memory of the answer, not the answer.
637
636
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pify/swarm",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Coordinate multiple pi agents in parallel: swarm_run fan-out with per-item auto-routing, concurrency queue, aggregated reports",
5
5
  "keywords": [
6
6
  "pi-package",
package/src/consent.ts CHANGED
@@ -16,6 +16,8 @@
16
16
  * around the first.
17
17
  */
18
18
 
19
+ import { readFileSync, writeFileSync } from "node:fs";
20
+
19
21
  /** What to do with a project-supplied file this session. */
20
22
  export type ConsentVerdict = "allow" | "refuse" | "ask";
21
23
 
@@ -80,6 +82,30 @@ export function writeConsent(
80
82
  return { ...file, [key]: { ...(file[key] ?? {}), [scope]: allowed } };
81
83
  }
82
84
 
85
+ /**
86
+ * Record a consent answer to disk without losing a concurrent writer's scope.
87
+ *
88
+ * Several @pify packages share this one file (keyed by cwd → {scope: bool}), and
89
+ * each asks the user with an `await` between reading the file and writing it
90
+ * back. If two flows both read the old file, then both write, the second write
91
+ * drops the scope the first added. So re-read and re-parse the file HERE, right
92
+ * before writing, with no `await` in between — the merge always starts from the
93
+ * freshest on-disk state, and within a single (single-threaded) process the
94
+ * read-modify-write can no longer interleave. Cross-process races remain
95
+ * theoretically possible but fail safe: the worst case is a re-prompt, never a
96
+ * silently-granted consent.
97
+ */
98
+ export function persistConsent(file: string, cwd: string, scope: string, allowed: boolean): void {
99
+ let raw: string | null = null;
100
+ try {
101
+ raw = readFileSync(file, "utf8");
102
+ } catch {
103
+ raw = null;
104
+ }
105
+ const next = writeConsent(parseConsent(raw), cwd, scope, allowed);
106
+ writeFileSync(file, `${JSON.stringify(next, null, 2)}\n`);
107
+ }
108
+
83
109
  /** Tolerate anything on disk: a corrupt consent file means "never asked". */
84
110
  export function parseConsent(raw: string | null): ConsentFile {
85
111
  if (!raw) return {};