@fitchmultz/pi-subagents 0.46.1 → 0.47.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 +29 -0
- package/README.md +1 -1
- package/dist/agents/agent-management.js +8 -2
- package/dist/agents/agents.js +8 -8
- package/dist/extension/build-info.js +1 -1
- package/dist/extension/index.js +11 -5
- package/dist/extension/result-snapshot.js +0 -18
- package/dist/history/ingest.js +40 -31
- package/dist/history/prune.js +1 -1
- package/dist/history/run-projection.js +1 -2
- package/dist/history/search-queries.js +8 -2
- package/dist/history/store.js +20 -4
- package/dist/history/text-chunks.js +1 -2
- package/dist/history/transcript-queries.js +12 -6
- package/dist/intercom/result-intercom.js +0 -74
- package/dist/pi-intercom/bridge-config.js +35 -26
- package/dist/pi-intercom/bridge-files.js +47 -0
- package/dist/pi-intercom/bridge-mailbox.js +324 -0
- package/dist/pi-intercom/bridge-presence.js +100 -0
- package/dist/pi-intercom/bridge-routes.js +27 -3
- package/dist/pi-intercom/bridge-session.js +60 -64
- package/dist/pi-intercom/bridge-wake.js +158 -0
- package/dist/pi-intercom/bridge.js +55 -7
- package/dist/pi-intercom/broker/broker.js +33 -16
- package/dist/pi-intercom/broker/framing.js +67 -17
- package/dist/pi-intercom/broker/snapshot-reader.js +29 -8
- package/dist/pi-intercom/broker/snapshots.js +144 -41
- package/dist/pi-intercom/session-targets.js +0 -1
- package/dist/runs/background/async-run-discovery.js +4 -6
- package/dist/runs/background/completion-dedupe.js +4 -55
- package/dist/runs/background/notify.js +1 -9
- package/dist/runs/background/result-watcher.js +1 -1
- package/dist/runs/background/runner-child-observer.js +12 -7
- package/dist/runs/background/runner-monitor.js +2 -0
- package/dist/runs/background/runner-parallel.js +2 -3
- package/dist/runs/shared/child-observation-spool.js +8 -3
- package/dist/runs/shared/run-restoration.js +4 -5
- package/dist/runs/shared/supervisor-questions.js +7 -4
- package/dist/shared/journal-frames.js +1 -1
- package/dist/slash/command-discovery.js +13 -12
- package/dist/tui/agent-history.js +0 -14
- package/dist/tui/agent-view.js +0 -1
- package/dist/tui/conversation-history.js +10 -1
- package/dist/tui/history-cards.js +8 -22
- package/docs/intercom-bridge.md +39 -17
- package/docs/intercom.md +5 -1
- package/docs/performance.md +72 -3
- package/package.json +1 -1
- package/scripts/intercom-bridge-credentials.mjs +54 -12
- package/scripts/intercom-remote.mjs +25 -1
- package/scripts/package-smoke.mjs +1 -1
- package/src/agents/agent-management.ts +8 -4
- package/src/agents/agents.ts +17 -12
- package/src/extension/index.ts +12 -5
- package/src/extension/result-snapshot.ts +1 -27
- package/src/history/ingest.ts +57 -49
- package/src/history/prune.ts +1 -1
- package/src/history/run-projection.ts +2 -4
- package/src/history/search-queries.ts +9 -2
- package/src/history/store.ts +21 -5
- package/src/history/text-chunks.ts +7 -2
- package/src/history/transcript-queries.ts +16 -10
- package/src/intercom/result-intercom.ts +0 -103
- package/src/pi-intercom/bridge-config.ts +46 -36
- package/src/pi-intercom/bridge-files.ts +59 -0
- package/src/pi-intercom/bridge-mailbox.ts +395 -0
- package/src/pi-intercom/bridge-presence.ts +128 -0
- package/src/pi-intercom/bridge-routes.ts +41 -4
- package/src/pi-intercom/bridge-session.ts +73 -71
- package/src/pi-intercom/bridge-wake.ts +188 -0
- package/src/pi-intercom/bridge.ts +58 -7
- package/src/pi-intercom/broker/broker.ts +34 -20
- package/src/pi-intercom/broker/framing.ts +71 -20
- package/src/pi-intercom/broker/snapshot-reader.ts +47 -9
- package/src/pi-intercom/broker/snapshots.ts +184 -51
- package/src/pi-intercom/session-targets.ts +0 -2
- package/src/runs/background/async-run-discovery.ts +4 -10
- package/src/runs/background/completion-dedupe.ts +6 -69
- package/src/runs/background/notify.ts +2 -12
- package/src/runs/background/result-watcher.ts +1 -1
- package/src/runs/background/runner-child-observer.ts +19 -11
- package/src/runs/background/runner-monitor.ts +2 -0
- package/src/runs/background/runner-parallel.ts +2 -3
- package/src/runs/shared/child-observation-spool.ts +8 -3
- package/src/runs/shared/run-restoration.ts +4 -4
- package/src/runs/shared/supervisor-questions.ts +10 -10
- package/src/shared/journal-frames.ts +1 -1
- package/src/slash/command-discovery.ts +13 -12
- package/src/tui/agent-history.ts +1 -18
- package/src/tui/agent-view.ts +0 -1
- package/src/tui/conversation-history.ts +15 -1
- package/src/tui/history-cards.ts +8 -26
|
@@ -58,7 +58,7 @@ export class TranscriptQueries {
|
|
|
58
58
|
boundary: Boundary,
|
|
59
59
|
ids: readonly string[],
|
|
60
60
|
): readonly (readonly [string, boolean])[] {
|
|
61
|
-
const
|
|
61
|
+
const humans: (readonly [string, number])[] = [];
|
|
62
62
|
const where = boundary.clauses.join(" AND ");
|
|
63
63
|
for (const id of new Set(ids)) {
|
|
64
64
|
const human = this.scope.store.get(
|
|
@@ -67,17 +67,23 @@ export class TranscriptQueries {
|
|
|
67
67
|
id,
|
|
68
68
|
);
|
|
69
69
|
if (human) {
|
|
70
|
-
|
|
71
|
-
id,
|
|
72
|
-
this.scope.store.get(
|
|
73
|
-
`SELECT rowid FROM entries WHERE ${where} AND start>? AND assistant_text=1 LIMIT 1`,
|
|
74
|
-
...boundary.params,
|
|
75
|
-
number(human, "start"),
|
|
76
|
-
) !== undefined,
|
|
77
|
-
]);
|
|
70
|
+
humans.push([id, number(human, "start")]);
|
|
78
71
|
}
|
|
79
72
|
}
|
|
80
|
-
|
|
73
|
+
if (humans.length === 0) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
// One tail lookup answers every ID, without scanning before the earliest requested human.
|
|
77
|
+
const assistant = this.scope.store.get(
|
|
78
|
+
`SELECT start FROM entries WHERE ${where} AND start>? AND assistant_text=1 ORDER BY start DESC LIMIT 1`,
|
|
79
|
+
...boundary.params,
|
|
80
|
+
Math.min(...humans.map(([, start]) => start)),
|
|
81
|
+
);
|
|
82
|
+
const assistantStart = assistant ? number(assistant, "start") : undefined;
|
|
83
|
+
return humans.map(([id, start]): readonly [string, boolean] => [
|
|
84
|
+
id,
|
|
85
|
+
assistantStart !== undefined && start < assistantStart,
|
|
86
|
+
]);
|
|
81
87
|
}
|
|
82
88
|
private readMetadata(
|
|
83
89
|
boundary: Boundary,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { ReadonlyInput } from "../shared/types/inputs.ts";
|
|
2
|
-
import { copyDetails, copyResult } from "../extension/result-snapshot.ts";
|
|
3
2
|
import { compactNestedResultChildren, formatNestedResultLines } from "./nested-result-tree.ts";
|
|
4
3
|
export {
|
|
5
4
|
compactNestedResultChildren,
|
|
@@ -13,7 +12,6 @@ import * as fs from "node:fs";
|
|
|
13
12
|
import { formatRunAction } from "../shared/status-format.ts";
|
|
14
13
|
import { SUBAGENT_CHILD_ENV, SUBAGENT_FANOUT_CHILD_ENV } from "../runs/shared/pi-args.ts";
|
|
15
14
|
import type {
|
|
16
|
-
Details,
|
|
17
15
|
SingleResult,
|
|
18
16
|
SubagentResultIntercomChild,
|
|
19
17
|
SubagentResultIntercomPayload,
|
|
@@ -308,104 +306,3 @@ export function buildSubagentResultIntercomPayload(
|
|
|
308
306
|
};
|
|
309
307
|
return { ...payload, message: formatSubagentResultIntercomMessage(payload) };
|
|
310
308
|
}
|
|
311
|
-
|
|
312
|
-
function stripSingleResultOutputs(result: ReadonlyInput<SingleResult>): SingleResult {
|
|
313
|
-
return {
|
|
314
|
-
...copyResult(result),
|
|
315
|
-
messages: undefined,
|
|
316
|
-
finalOutput: undefined,
|
|
317
|
-
truncation: undefined,
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
export function stripDetailsOutputsForIntercomReceipt(
|
|
322
|
-
details: ReadonlyInput<Details>,
|
|
323
|
-
delivery?: Details["intercomDelivery"],
|
|
324
|
-
): Details {
|
|
325
|
-
return {
|
|
326
|
-
...copyDetails(details),
|
|
327
|
-
results: details.results.map(stripSingleResultOutputs),
|
|
328
|
-
...(delivery ? { intercomDelivery: delivery } : {}),
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function compactReceiptSummary(summary: string): string {
|
|
333
|
-
const withoutOutput = summary.split(/\n\nOutput:\n|\sOutput:\s/)[0] ?? summary;
|
|
334
|
-
const normalized = withoutOutput.replace(/\s+/g, " ").trim();
|
|
335
|
-
return normalized.length > 240 ? `${normalized.slice(0, 239)}…` : normalized;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function formatChildReferences(
|
|
339
|
-
children: readonly SubagentResultIntercomChild[],
|
|
340
|
-
field: "artifactPath" | "intercomTarget" | "sessionPath",
|
|
341
|
-
heading: string,
|
|
342
|
-
): string[] {
|
|
343
|
-
const lines: string[] = [];
|
|
344
|
-
for (const child of children) {
|
|
345
|
-
const value = child[field];
|
|
346
|
-
if (typeof value === "string") {
|
|
347
|
-
lines.push(`- ${child.agent} [${child.status}]: ${value}`);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
return lines.length > 0 ? [heading, ...lines] : [];
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function formatReceiptReferences(payload: SubagentResultIntercomPayload): string[] {
|
|
354
|
-
const lines: string[] = [];
|
|
355
|
-
if (hasText(payload.resultPath)) {
|
|
356
|
-
lines.push(`Saved result (acceptance details when configured): ${payload.resultPath}`);
|
|
357
|
-
}
|
|
358
|
-
for (const child of payload.children) {
|
|
359
|
-
if (hasText(child.metadataPath)) {
|
|
360
|
-
lines.push(
|
|
361
|
-
`Result metadata (${child.agent}; acceptance details when configured): ${child.metadataPath}${fs.existsSync(child.metadataPath) ? "" : " (missing)"}`,
|
|
362
|
-
);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
lines.push(
|
|
366
|
-
...formatChildReferences(payload.children, "artifactPath", "Artifacts:"),
|
|
367
|
-
...formatChildReferences(
|
|
368
|
-
payload.children,
|
|
369
|
-
"intercomTarget",
|
|
370
|
-
"Run intercom targets (may be inactive after completion):",
|
|
371
|
-
),
|
|
372
|
-
...formatChildReferences(payload.children, "sessionPath", "Sessions:"),
|
|
373
|
-
);
|
|
374
|
-
const nonCompleted = payload.children.filter((child) => child.status !== "completed");
|
|
375
|
-
if (nonCompleted.length > 0) {
|
|
376
|
-
lines.push("Non-completed children:");
|
|
377
|
-
for (const child of nonCompleted) {
|
|
378
|
-
lines.push(`- ${child.agent} [${child.status}]: ${compactReceiptSummary(child.summary)}`);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
return lines;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
export function formatSubagentResultReceipt(input: {
|
|
385
|
-
readonly mode: SubagentRunMode;
|
|
386
|
-
readonly runId: string;
|
|
387
|
-
readonly payload: SubagentResultIntercomPayload;
|
|
388
|
-
}): string {
|
|
389
|
-
const counts = countStatuses(input.payload.children);
|
|
390
|
-
const modeLabel = {
|
|
391
|
-
single: "single subagent result",
|
|
392
|
-
parallel: "parallel subagent results",
|
|
393
|
-
chain: "chain subagent results",
|
|
394
|
-
}[input.mode];
|
|
395
|
-
const lines = [
|
|
396
|
-
`Delivered ${modeLabel} via intercom.`,
|
|
397
|
-
"Delivery: succeeded",
|
|
398
|
-
`Run: ${input.runId}`,
|
|
399
|
-
`Child outcome: ${input.payload.status}`,
|
|
400
|
-
`Children: ${formatStatusCounts(counts)}`,
|
|
401
|
-
...(hasText(input.payload.error)
|
|
402
|
-
? [
|
|
403
|
-
`Workflow ${input.payload.status === "paused" ? "paused" : "error"}: ${input.payload.error}`,
|
|
404
|
-
]
|
|
405
|
-
: []),
|
|
406
|
-
];
|
|
407
|
-
|
|
408
|
-
lines.push(...formatReceiptReferences(input.payload));
|
|
409
|
-
lines.push("Full grouped output was sent over intercom.");
|
|
410
|
-
return lines.join("\n");
|
|
411
|
-
}
|
|
@@ -1,27 +1,51 @@
|
|
|
1
|
-
import {
|
|
2
|
-
constants,
|
|
3
|
-
closeSync,
|
|
4
|
-
fstatSync,
|
|
5
|
-
openSync,
|
|
6
|
-
readFileSync,
|
|
7
|
-
realpathSync,
|
|
8
|
-
statSync,
|
|
9
|
-
} from "node:fs";
|
|
1
|
+
import { realpathSync, statSync } from "node:fs";
|
|
10
2
|
import { isAbsolute } from "node:path";
|
|
11
3
|
import { createSecureContext } from "node:tls";
|
|
4
|
+
import { parseEnv } from "node:util";
|
|
12
5
|
import { isUnknownArray } from "../shared/unknown.ts";
|
|
13
6
|
import { BODY_LIMIT, object, label, fingerprint } from "./bridge-protocol.ts";
|
|
14
7
|
import { launchPolicy, type LaunchPolicy } from "./bridge-launch-policy.ts";
|
|
8
|
+
import { stateDirectory } from "./bridge-mailbox.ts";
|
|
9
|
+
import { readBridgeFile } from "./bridge-files.ts";
|
|
15
10
|
|
|
16
11
|
const MAX_CLIENTS = 64;
|
|
17
12
|
|
|
13
|
+
export interface WakePolicy {
|
|
14
|
+
readonly envFile: string;
|
|
15
|
+
readonly url: string;
|
|
16
|
+
readonly key: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function wakePolicy(value: unknown): WakePolicy | undefined {
|
|
20
|
+
if (value === undefined) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const input = object(value, ["envFile"]);
|
|
24
|
+
const envFile = label(input.envFile, "wake credential file", 4096);
|
|
25
|
+
if (!isAbsolute(envFile)) {
|
|
26
|
+
throw new Error("Wake credential file must be absolute.");
|
|
27
|
+
}
|
|
28
|
+
const env = parseEnv(readBridgeFile(envFile, true, 16384).toString("utf8"));
|
|
29
|
+
const url = new URL(label(env.GROK_WAKE_WEBHOOK_URL, "wake webhook URL", 8192));
|
|
30
|
+
if (url.protocol !== "https:" || url.username !== "" || url.password !== "" || url.hash !== "") {
|
|
31
|
+
throw new Error("Wake webhook requires HTTPS without URL credentials or a fragment.");
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
envFile,
|
|
35
|
+
url: url.href,
|
|
36
|
+
key: label(env.GROK_WAKE_WEBHOOK_KEY, "wake webhook key", 4096),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
18
40
|
export interface Identity {
|
|
19
41
|
readonly fingerprint256: string;
|
|
20
42
|
readonly name: string;
|
|
21
43
|
readonly cwd: string;
|
|
22
44
|
readonly launch?: LaunchPolicy;
|
|
45
|
+
readonly wake?: WakePolicy;
|
|
23
46
|
}
|
|
24
47
|
export interface Config {
|
|
48
|
+
readonly stateDir?: string;
|
|
25
49
|
readonly port: number;
|
|
26
50
|
readonly ca: string;
|
|
27
51
|
readonly cert: string;
|
|
@@ -30,26 +54,6 @@ export interface Config {
|
|
|
30
54
|
readonly tls: { readonly ca: Buffer; readonly cert: Buffer; readonly key: Buffer };
|
|
31
55
|
}
|
|
32
56
|
|
|
33
|
-
function readFile(path: string, privateFile: boolean, limit: number): Buffer {
|
|
34
|
-
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
35
|
-
try {
|
|
36
|
-
const stat = fstatSync(fd);
|
|
37
|
-
if (!stat.isFile() || stat.size > limit) {
|
|
38
|
-
throw new Error("Configuration/TLS file must be a bounded regular file.");
|
|
39
|
-
}
|
|
40
|
-
if (privateFile && (stat.uid !== process.getuid?.() || (stat.mode & 0o777) !== 0o600)) {
|
|
41
|
-
throw new Error("Config and TLS key must be owned by the current user with mode 0600.");
|
|
42
|
-
}
|
|
43
|
-
const data = readFileSync(fd);
|
|
44
|
-
if (data.length > limit) {
|
|
45
|
-
throw new Error("Configuration/TLS file exceeds its size limit.");
|
|
46
|
-
}
|
|
47
|
-
return data;
|
|
48
|
-
} finally {
|
|
49
|
-
closeSync(fd);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
57
|
function identities(value: unknown): Map<string, Identity> {
|
|
54
58
|
if (!isUnknownArray(value) || value.length > MAX_CLIENTS) {
|
|
55
59
|
throw new Error(`clients must be an array of at most ${MAX_CLIENTS} identities.`);
|
|
@@ -57,7 +61,7 @@ function identities(value: unknown): Map<string, Identity> {
|
|
|
57
61
|
const clients = new Map<string, Identity>();
|
|
58
62
|
const names = new Set<string>();
|
|
59
63
|
for (const raw of value) {
|
|
60
|
-
const entry = object(raw, ["fingerprint256", "name", "cwd", "launch"]);
|
|
64
|
+
const entry = object(raw, ["fingerprint256", "name", "cwd", "launch", "wake"]);
|
|
61
65
|
const fingerprint256 = fingerprint(entry.fingerprint256);
|
|
62
66
|
const name = label(entry.name, "name", 128).trim();
|
|
63
67
|
const cwd = label(entry.cwd, "cwd", 4096);
|
|
@@ -76,6 +80,7 @@ function identities(value: unknown): Map<string, Identity> {
|
|
|
76
80
|
name,
|
|
77
81
|
cwd: launch ? realpathSync(cwd) : cwd,
|
|
78
82
|
launch,
|
|
83
|
+
wake: wakePolicy(entry.wake),
|
|
79
84
|
});
|
|
80
85
|
names.add(name.toLowerCase());
|
|
81
86
|
}
|
|
@@ -84,9 +89,9 @@ function identities(value: unknown): Map<string, Identity> {
|
|
|
84
89
|
|
|
85
90
|
export function loadConfig(path: string): Config {
|
|
86
91
|
const raw: unknown = JSON.parse(
|
|
87
|
-
new TextDecoder("utf-8", { fatal: true }).decode(
|
|
92
|
+
new TextDecoder("utf-8", { fatal: true }).decode(readBridgeFile(path, true, BODY_LIMIT)),
|
|
88
93
|
);
|
|
89
|
-
const input = object(raw, ["port", "ca", "cert", "key", "clients"]);
|
|
94
|
+
const input = object(raw, ["port", "ca", "cert", "key", "clients", "stateDir"]);
|
|
90
95
|
const port = input.port === undefined ? 9443 : input.port;
|
|
91
96
|
if (typeof port !== "number" || !Number.isInteger(port) || port < 0 || port > 65535) {
|
|
92
97
|
throw new Error("Invalid HTTPS port.");
|
|
@@ -99,16 +104,21 @@ export function loadConfig(path: string): Config {
|
|
|
99
104
|
}
|
|
100
105
|
const clients = identities(input.clients);
|
|
101
106
|
const tls = {
|
|
102
|
-
ca:
|
|
103
|
-
cert:
|
|
104
|
-
key:
|
|
107
|
+
ca: readBridgeFile(ca, false, 1024 * 1024),
|
|
108
|
+
cert: readBridgeFile(cert, false, 1024 * 1024),
|
|
109
|
+
key: readBridgeFile(key, true, 1024 * 1024),
|
|
105
110
|
};
|
|
106
111
|
createSecureContext({ ...tls, minVersion: "TLSv1.2" });
|
|
107
|
-
|
|
112
|
+
const stateDir = stateDirectory(input.stateDir);
|
|
113
|
+
if (stateDir === undefined && [...clients.values()].some((identity) => identity.wake)) {
|
|
114
|
+
throw new Error("Wake delivery requires a durable stateDir.");
|
|
115
|
+
}
|
|
116
|
+
return { port, ca, cert, key, clients, tls, stateDir };
|
|
108
117
|
}
|
|
109
118
|
|
|
110
119
|
export function sameListener(next: Config, current: Config): boolean {
|
|
111
120
|
return (
|
|
121
|
+
next.stateDir === current.stateDir &&
|
|
112
122
|
next.port === current.port &&
|
|
113
123
|
next.ca === current.ca &&
|
|
114
124
|
next.cert === current.cert &&
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {
|
|
2
|
+
constants,
|
|
3
|
+
closeSync,
|
|
4
|
+
fstatSync,
|
|
5
|
+
fsyncSync,
|
|
6
|
+
openSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
renameSync,
|
|
9
|
+
rmSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from "node:fs";
|
|
12
|
+
import { dirname } from "node:path";
|
|
13
|
+
import { randomUUID } from "node:crypto";
|
|
14
|
+
|
|
15
|
+
export function readBridgeFile(path: string, privateFile: boolean, limit: number): Buffer {
|
|
16
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
17
|
+
try {
|
|
18
|
+
const stat = fstatSync(fd);
|
|
19
|
+
if (!stat.isFile() || stat.size > limit) {
|
|
20
|
+
throw new Error("Configuration/TLS file must be a bounded regular file.");
|
|
21
|
+
}
|
|
22
|
+
if (privateFile && (stat.uid !== process.getuid?.() || (stat.mode & 0o777) !== 0o600)) {
|
|
23
|
+
throw new Error("Config and TLS key must be owned by the current user with mode 0600.");
|
|
24
|
+
}
|
|
25
|
+
const data = readFileSync(fd);
|
|
26
|
+
if (data.length > limit) {
|
|
27
|
+
throw new Error("Configuration/TLS file exceeds its size limit.");
|
|
28
|
+
}
|
|
29
|
+
return data;
|
|
30
|
+
} finally {
|
|
31
|
+
closeSync(fd);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function writeBridgeJson(path: string, value: object): void {
|
|
36
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
37
|
+
const fd = openSync(
|
|
38
|
+
temporary,
|
|
39
|
+
constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,
|
|
40
|
+
0o600,
|
|
41
|
+
);
|
|
42
|
+
try {
|
|
43
|
+
try {
|
|
44
|
+
writeFileSync(fd, JSON.stringify(value));
|
|
45
|
+
fsyncSync(fd);
|
|
46
|
+
} finally {
|
|
47
|
+
closeSync(fd);
|
|
48
|
+
}
|
|
49
|
+
renameSync(temporary, path);
|
|
50
|
+
const directory = openSync(dirname(path), constants.O_RDONLY | constants.O_DIRECTORY);
|
|
51
|
+
try {
|
|
52
|
+
fsyncSync(directory);
|
|
53
|
+
} finally {
|
|
54
|
+
closeSync(directory);
|
|
55
|
+
}
|
|
56
|
+
} finally {
|
|
57
|
+
rmSync(temporary, { force: true });
|
|
58
|
+
}
|
|
59
|
+
}
|