@intentic/constants 1.323.0 → 1.325.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/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/heavy/heavy-rules.cjs +34 -0
- package/src/heavy/heavy-rules.d.cts +4 -0
- package/src/heavy/heavy-turn.cjs +4 -3
- package/src/memory-room.d.mts +12 -3
- package/src/memory-room.mjs +60 -18
package/dist/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export declare const PUBLIC_DIR = "public";
|
|
|
10
10
|
export declare const MEMORY_FILE = "AGENTS.md";
|
|
11
11
|
export declare const FIELD_NOTES_FILE = ".intentic/config/field-notes.toon";
|
|
12
12
|
export declare const HOST_STATE_ROOT = "/opt/intentic";
|
|
13
|
-
export declare const LEGAL_VERSION = "2026-
|
|
13
|
+
export declare const LEGAL_VERSION = "2026-10-04";
|
|
14
14
|
export declare const LEGAL_CONTACT_EMAIL = "contact@intentic.dev";
|
|
15
15
|
export declare const LEGAL_ENTITY_NAME = "Artur Kurowski, trading as radarsu";
|
|
16
16
|
export declare const LEGAL_ENTITY_COUNTRY = "Poland";
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ export const PUBLIC_DIR = "public";
|
|
|
10
10
|
export const MEMORY_FILE = "AGENTS.md";
|
|
11
11
|
export const FIELD_NOTES_FILE = ".intentic/config/field-notes.toon";
|
|
12
12
|
export const HOST_STATE_ROOT = "/opt/intentic";
|
|
13
|
-
export const LEGAL_VERSION = "2026-
|
|
13
|
+
export const LEGAL_VERSION = "2026-10-04";
|
|
14
14
|
export const LEGAL_CONTACT_EMAIL = "contact@intentic.dev";
|
|
15
15
|
export const LEGAL_ENTITY_NAME = "Artur Kurowski, trading as radarsu";
|
|
16
16
|
export const LEGAL_ENTITY_COUNTRY = "Poland";
|
package/package.json
CHANGED
|
@@ -186,6 +186,38 @@ const queueArgs = (match, config) => [
|
|
|
186
186
|
match.id,
|
|
187
187
|
];
|
|
188
188
|
|
|
189
|
+
// A test run that names what it runs: a word after the test verb that reads as a path, a test file or a filter
|
|
190
|
+
// (`pnpm --filter web test src/router/signIn.test.ts`, `bun test --conditions=x src/a.test.ts`, `vitest run tools/`,
|
|
191
|
+
// `pnpm --filter @intentic/sandbox test filter-stats`). The
|
|
192
|
+
// memory gate prices it as one worker over a few files (memory-room.mjs TARGETED_RUN_BYTES), not as a package's
|
|
193
|
+
// suite: measured 2026-09-26..10-02, 2,147 of 2,463 test runs named their files, and the gate held them a median 6.4 s
|
|
194
|
+
// for work that takes under one. A recursive or turbo fan-out is never targeted, whatever it names.
|
|
195
|
+
const TEST_VERB = /^(test|vitest|jest)$/u;
|
|
196
|
+
const FAN_OUT = /^(-r|--recursive|turbo|--workspaces?|-ws)$/u;
|
|
197
|
+
const PATHLIKE = /[/.]\p{L}|^\p{L}[\w-]*\.(test|spec)\b|\b(test|spec)s?\b/u;
|
|
198
|
+
const targetedRun = (invocation) => {
|
|
199
|
+
const words = invocation.slice(0, MATCH_LIMIT).split(/\s+/u).filter((word) => word !== "");
|
|
200
|
+
if (words.some((word) => FAN_OUT.test(word))) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
const verb = words.findIndex((word, index) => TEST_VERB.test(word.split("/").pop() ?? "") && (index > 0 || word !== "test"));
|
|
204
|
+
if (verb === -1) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
// A bare word (`filter-stats`) is a filter too, unless it may be the value of the flag before it (`--reporter dot`).
|
|
208
|
+
return words.slice(verb + 1).some((word, index, rest) => {
|
|
209
|
+
if (word.startsWith("-") || word.startsWith("@") || word === "run" || !/\p{L}/u.test(word)) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
const before = rest[index - 1] ?? "";
|
|
213
|
+
return PATHLIKE.test(word) || !(before.startsWith("-") && before !== "--" && !before.includes("="));
|
|
214
|
+
});
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// queue-run's `--size` for a match: `targeted` for a test run aimed at named files, nothing for everything else.
|
|
218
|
+
const sizeArgs = (match, invocation) =>
|
|
219
|
+
["vitest", "package-script"].includes(match.id) && targetedRun(invocation) ? ["--size", "targeted"] : [];
|
|
220
|
+
|
|
189
221
|
// When a slot held this long is worth a line in the log: half of what its own rule lets it hold, never for a rule that
|
|
190
222
|
// lets it hold forever.
|
|
191
223
|
const holdWarnSeconds = (maxHold) => (maxHold > 0 ? maxHold / 2 : undefined);
|
|
@@ -202,6 +234,8 @@ module.exports = {
|
|
|
202
234
|
overridesOf,
|
|
203
235
|
matchInvocation,
|
|
204
236
|
queueArgs,
|
|
237
|
+
targetedRun,
|
|
238
|
+
sizeArgs,
|
|
205
239
|
holdWarnSeconds,
|
|
206
240
|
ruleById,
|
|
207
241
|
};
|
|
@@ -67,5 +67,9 @@ export function mergeHeavyRules(overrides?: HeavyCommandOverrides): HeavyCommand
|
|
|
67
67
|
export function overridesOf(full: Partial<HeavyCommandSettings> & { readonly rules?: readonly HeavyCommandRule[] | undefined }): HeavyCommandOverrides;
|
|
68
68
|
export function matchInvocation(invocation: string, config: HeavyCommands, report?: (detail: string) => void): HeavyMatch | undefined;
|
|
69
69
|
export function queueArgs(match: HeavyMatch, config: HeavyCommandSettings): string[];
|
|
70
|
+
// Whether a test invocation names the files it runs, for the memory gate's smaller price.
|
|
71
|
+
export function targetedRun(invocation: string): boolean;
|
|
72
|
+
// queue-run's `--size targeted` for a targeted test run under a test rule, else nothing.
|
|
73
|
+
export function sizeArgs(match: HeavyMatch, invocation: string): string[];
|
|
70
74
|
export function holdWarnSeconds(maxHold: number): number | undefined;
|
|
71
75
|
export function ruleById(config: HeavyCommands, id: string): HeavyCommandRule | undefined;
|
package/src/heavy/heavy-turn.cjs
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
const { spawnSync } = require("node:child_process");
|
|
16
16
|
const { accessSync, constants, readFileSync, writeFileSync } = require("node:fs");
|
|
17
17
|
const { getPriority, setPriority } = require("node:os");
|
|
18
|
-
const { matchInvocation, queueArgs } = require("./heavy-rules.cjs");
|
|
18
|
+
const { matchInvocation, queueArgs, sizeArgs } = require("./heavy-rules.cjs");
|
|
19
19
|
|
|
20
20
|
// The spec the daemon handed down, or undefined when this program is not under it or is already covered.
|
|
21
21
|
const pendingSpec = (env = process.env) => {
|
|
@@ -92,13 +92,14 @@ const takeTurn = ({ program, args, command, env = process.env }) => {
|
|
|
92
92
|
if (spec === undefined) {
|
|
93
93
|
return false;
|
|
94
94
|
}
|
|
95
|
-
const
|
|
95
|
+
const invocation = [program, ...args].join(" ");
|
|
96
|
+
const match = matchInvocation(invocation, spec.rules);
|
|
96
97
|
if (match === undefined) {
|
|
97
98
|
return false;
|
|
98
99
|
}
|
|
99
100
|
env.INTENTIC_HEAVY_HELD = match.id;
|
|
100
101
|
applyClass(spec.klass);
|
|
101
|
-
const queue = spec.queue === true && typeof spec.queueRun === "string" && runnable(spec.queueRun) ? [spec.queueRun, ...queueArgs(match, spec.rules), "--"] : [];
|
|
102
|
+
const queue = spec.queue === true && typeof spec.queueRun === "string" && runnable(spec.queueRun) ? [spec.queueRun, ...queueArgs(match, spec.rules), ...sizeArgs(match, invocation), "--"] : [];
|
|
102
103
|
const runner = spec.offload?.[match.id];
|
|
103
104
|
if (runner !== undefined && typeof spec.offloadRun === "string") {
|
|
104
105
|
const here = queue.length === 0 ? "" : `${queue.map(quote).join(" ")} `;
|
package/src/memory-room.d.mts
CHANGED
|
@@ -3,6 +3,9 @@ export type RoomWorkload = "agentRuntime" | "service" | "panel" | "install" | "c
|
|
|
3
3
|
|
|
4
4
|
export const COST_BYTES: Readonly<Record<RoomWorkload, number>>;
|
|
5
5
|
export const PERSON_RESERVE_BYTES: number;
|
|
6
|
+
export const TARGETED_RUN_BYTES: number;
|
|
7
|
+
// A toolchain command aimed at named files, priced at TARGETED_RUN_BYTES instead of its class's cost.
|
|
8
|
+
export type RoomSize = "targeted";
|
|
6
9
|
export const TEST_PROCESS_BYTES: Readonly<{ fanOutWorker: number; standaloneWorker: number; typecheck: number; ceiling: number }>;
|
|
7
10
|
export const STALL_PERCENT: number;
|
|
8
11
|
export const RESERVATION_MS: number;
|
|
@@ -12,7 +15,7 @@ export const READING_FILES: readonly string[];
|
|
|
12
15
|
export interface MemoryReading {
|
|
13
16
|
// memory.high, else memory.max, else the machine's memory; undefined where nothing bounds it.
|
|
14
17
|
readonly limitBytes: number | undefined;
|
|
15
|
-
// Working set plus swap; undefined where nothing measures it.
|
|
18
|
+
// Working set plus swap; undefined where nothing measures it. Free memory is counted against its resident part.
|
|
16
19
|
readonly usedBytes: number | undefined;
|
|
17
20
|
// The swapped part of `usedBytes`; 0 when swap is off or unaccounted.
|
|
18
21
|
readonly swapBytes: number;
|
|
@@ -50,8 +53,12 @@ export interface RoomAnswer extends RoomJudgement {
|
|
|
50
53
|
|
|
51
54
|
export function readingFrom(read: (path: string) => string | undefined): MemoryReading;
|
|
52
55
|
export function readReadingSync(): MemoryReading;
|
|
53
|
-
export function
|
|
54
|
-
export function
|
|
56
|
+
export function costBytesOf(workload: RoomWorkload, size?: RoomSize): number;
|
|
57
|
+
export function needBytes(workload: RoomWorkload, attended: boolean, size?: RoomSize): number;
|
|
58
|
+
export function judge(
|
|
59
|
+
reading: MemoryReading,
|
|
60
|
+
request: { readonly workload: RoomWorkload; readonly attended: boolean; readonly reservedBytes?: number; readonly size?: RoomSize | undefined },
|
|
61
|
+
): RoomJudgement;
|
|
55
62
|
export function freeBytesOf(reading: MemoryReading): number | undefined;
|
|
56
63
|
export function askRoom(options?: {
|
|
57
64
|
readonly workload?: RoomWorkload;
|
|
@@ -60,6 +67,8 @@ export function askRoom(options?: {
|
|
|
60
67
|
readonly socketPath?: string;
|
|
61
68
|
readonly intervalMs?: number;
|
|
62
69
|
readonly read?: () => MemoryReading;
|
|
70
|
+
readonly size?: RoomSize;
|
|
71
|
+
readonly pid?: number;
|
|
63
72
|
}): Promise<RoomAnswer>;
|
|
64
73
|
export function askFree(options?: { readonly socketPath?: string; readonly read?: () => MemoryReading }): Promise<{
|
|
65
74
|
readonly freeBytes: number | undefined;
|
package/src/memory-room.mjs
CHANGED
|
@@ -9,8 +9,12 @@
|
|
|
9
9
|
// memory, never more than the machine's
|
|
10
10
|
// used the working set (memory.current less the inactive file cache the kernel reclaims first) plus what was
|
|
11
11
|
// pushed to swap; at a root cgroup, which has no memory.current, the machine's own used memory and swap
|
|
12
|
-
// free limit − used, and never more than the machine itself has available (MemAvailable): a
|
|
13
|
-
// shared machine cannot take memory the machine does not have, whatever its own limit says
|
|
12
|
+
// free limit − the resident part of used, and never more than the machine itself has available (MemAvailable): a
|
|
13
|
+
// sandbox on a shared machine cannot take memory the machine does not have, whatever its own limit says.
|
|
14
|
+
// Swap is not subtracted: the limit bounds resident pages only, and cold pages parked in swap leave that room
|
|
15
|
+
// free. Counting them held every heavy command for minutes on a sandbox with 8 GB available (measured
|
|
16
|
+
// 2026-09-26..10-02: 4.2 GiB of idle swap, 17 h of gate waits a week). Swap that is being paged back in shows
|
|
17
|
+
// as stall, below, which holds work whatever the bytes say.
|
|
14
18
|
// stall memory PSI `full avg10`: the share of the last ten seconds in which everything waited on memory
|
|
15
19
|
import { spawnSync } from "node:child_process";
|
|
16
20
|
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
@@ -34,6 +38,11 @@ export const COST_BYTES = Object.freeze({
|
|
|
34
38
|
toolchain: GIB,
|
|
35
39
|
});
|
|
36
40
|
|
|
41
|
+
// What a toolchain command aimed at named files takes: one bun worker over one test file, not a package's suite
|
|
42
|
+
// (measured 2026-10-02: a 63-test file ran in 16 ms well under 300 MiB). heavy-rules.cjs `targetedRun` decides it from
|
|
43
|
+
// the invocation, and queue-run passes it as `--size targeted`.
|
|
44
|
+
export const TARGETED_RUN_BYTES = 512 * MIB;
|
|
45
|
+
|
|
37
46
|
// What one test or typecheck process holds at its peak, for the scripts that size a fan-out to the free memory
|
|
38
47
|
// (_tools/scripts/verify/test-workers.mjs) and the ceiling that stops a runaway one (_tools/scripts/lib/memory-ceiling.mjs).
|
|
39
48
|
export const TEST_PROCESS_BYTES = Object.freeze({
|
|
@@ -139,16 +148,20 @@ export const readReadingSync = () => readingFrom(readSync);
|
|
|
139
148
|
|
|
140
149
|
const gib = (bytes) => `${(bytes / GIB).toFixed(1)} GiB`;
|
|
141
150
|
|
|
151
|
+
/** What one more of `workload` costs: its class's cost, or a targeted run's where `size` says so. */
|
|
152
|
+
export const costBytesOf = (workload, size) =>
|
|
153
|
+
workload === "toolchain" && size === "targeted" ? TARGETED_RUN_BYTES : (COST_BYTES[workload] ?? COST_BYTES.agentRuntime);
|
|
154
|
+
|
|
142
155
|
/** What starting one more of `workload` needs free: its own cost, plus a person's turn when nobody is waiting on it. */
|
|
143
|
-
export const needBytes = (workload, attended) => (
|
|
156
|
+
export const needBytes = (workload, attended, size) => costBytesOf(workload, size) + (attended ? 0 : PERSON_RESERVE_BYTES);
|
|
144
157
|
|
|
145
158
|
/**
|
|
146
159
|
* The verdict for one more of `workload`: `run`, or, when the sandbox is short, `refuse` for work a person is waiting
|
|
147
160
|
* on (they are told, and decide) and `wait` for work nobody is (it is held until there is room). `reservedBytes` is what
|
|
148
161
|
* other admissions still hold off the reading. An unmeasurable sandbox has no opinion, and runs.
|
|
149
162
|
*/
|
|
150
|
-
export const judge = (reading, { workload, attended, reservedBytes = 0 }) => {
|
|
151
|
-
const need = needBytes(workload, attended);
|
|
163
|
+
export const judge = (reading, { workload, attended, reservedBytes = 0, size }) => {
|
|
164
|
+
const need = needBytes(workload, attended, size);
|
|
152
165
|
const { limitBytes, usedBytes, swapBytes, stallPercent, availableBytes } = reading;
|
|
153
166
|
const unreserved = freeBytesOf(reading);
|
|
154
167
|
if (limitBytes === undefined || usedBytes === undefined || unreserved === undefined) {
|
|
@@ -171,12 +184,12 @@ export const judge = (reading, { workload, attended, reservedBytes = 0 }) => {
|
|
|
171
184
|
return { verdict: "run", needBytes: need, freeBytes, reservedBytes };
|
|
172
185
|
}
|
|
173
186
|
// The machine, not the sandbox's own limit, is what ran out: saying "9 of 16 GiB used" would read as a wrong refusal.
|
|
174
|
-
if (availableBytes !== undefined && availableBytes < limitBytes - usedBytes) {
|
|
187
|
+
if (availableBytes !== undefined && availableBytes < limitBytes - (usedBytes - swapBytes)) {
|
|
175
188
|
const held = reservedBytes > 0 ? `, and ${gib(reservedBytes)} held for work that just started` : "";
|
|
176
189
|
return shortOf(`Sandbox memory is low: the machine it runs on has ${gib(availableBytes)} available${held}`);
|
|
177
190
|
}
|
|
178
191
|
// Resident and swapped are named apart once paging starts: the limit bounds resident pages only, so their sum can
|
|
179
|
-
// exceed it, and "19.2 GiB of 16.0 GiB used" reads as a bug.
|
|
192
|
+
// exceed it, and "19.2 GiB of 16.0 GiB used" reads as a bug. Free is counted against the resident part alone.
|
|
180
193
|
const used =
|
|
181
194
|
swapBytes > 0
|
|
182
195
|
? `${gib(usedBytes - swapBytes)} resident + ${gib(swapBytes)} swapped, against ${gib(limitBytes)}`
|
|
@@ -186,17 +199,26 @@ export const judge = (reading, { workload, attended, reservedBytes = 0 }) => {
|
|
|
186
199
|
return shortOf(`Sandbox memory is low: ${used}${held}`, { limitBytes, residentBytes: usedBytes - swapBytes, swapBytes });
|
|
187
200
|
};
|
|
188
201
|
|
|
189
|
-
/** Free memory as the formula counts it, or undefined where nothing bounds or measures it. */
|
|
202
|
+
/** Free memory as the formula counts it (the limit less what is resident), or undefined where nothing bounds or measures it. */
|
|
190
203
|
export const freeBytesOf = (reading) =>
|
|
191
204
|
reading.limitBytes === undefined || reading.usedBytes === undefined
|
|
192
205
|
? undefined
|
|
193
|
-
: Math.max(
|
|
206
|
+
: Math.max(
|
|
207
|
+
0,
|
|
208
|
+
Math.min(reading.limitBytes - (reading.usedBytes - (reading.swapBytes ?? 0)), reading.availableBytes ?? Number.POSITIVE_INFINITY),
|
|
209
|
+
);
|
|
194
210
|
|
|
195
211
|
// ---- asking the daemon, with the formula itself as the fallback ----
|
|
196
212
|
|
|
197
|
-
const askSocket = (workload, waitSeconds, label, socketPath) =>
|
|
213
|
+
const askSocket = (workload, waitSeconds, label, socketPath, size, pid) =>
|
|
198
214
|
new Promise((resolve) => {
|
|
199
215
|
const query = new URLSearchParams({ class: workload, wait: String(waitSeconds), label });
|
|
216
|
+
if (size !== undefined) {
|
|
217
|
+
query.set("size", size);
|
|
218
|
+
}
|
|
219
|
+
if (pid !== undefined) {
|
|
220
|
+
query.set("pid", String(pid));
|
|
221
|
+
}
|
|
200
222
|
const asked = request({ socketPath, path: `/room?${query}`, method: "GET", timeout: (waitSeconds + 15) * 1000 }, (response) => {
|
|
201
223
|
let body = "";
|
|
202
224
|
response.setEncoding("utf8");
|
|
@@ -266,24 +288,36 @@ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
266
288
|
/**
|
|
267
289
|
* The daemon's verdict for one more of `workload`, waiting up to `waitSeconds` for room; where no daemon answers, the
|
|
268
290
|
* same formula over the same files, polled every `intervalMs`. Never throws: every failure is an answer of `run`.
|
|
291
|
+
* `size: "targeted"` prices a toolchain command aimed at named files (TARGETED_RUN_BYTES); `pid` is the process the
|
|
292
|
+
* admitted work runs as, whose exit hands its reservation back at once instead of after RESERVATION_MS.
|
|
269
293
|
*/
|
|
270
|
-
export const askRoom = async ({
|
|
271
|
-
|
|
294
|
+
export const askRoom = async ({
|
|
295
|
+
workload = "toolchain",
|
|
296
|
+
waitSeconds = 0,
|
|
297
|
+
label = "command",
|
|
298
|
+
socketPath = ROOM_SOCKET,
|
|
299
|
+
intervalMs = 5_000,
|
|
300
|
+
read = readReadingSync,
|
|
301
|
+
size,
|
|
302
|
+
pid,
|
|
303
|
+
} = {}) => {
|
|
304
|
+
const answered = await askSocket(workload, waitSeconds, label, socketPath, size, pid);
|
|
272
305
|
if (answered !== undefined) {
|
|
273
306
|
return { ...answered, source: "daemon" };
|
|
274
307
|
}
|
|
275
308
|
const startedAt = Date.now();
|
|
276
|
-
let verdict = judge(read(), { workload, attended: false });
|
|
309
|
+
let verdict = judge(read(), { workload, attended: false, size });
|
|
277
310
|
while (verdict.verdict !== "run" && Date.now() - startedAt < waitSeconds * 1000) {
|
|
278
311
|
await sleep(intervalMs);
|
|
279
|
-
verdict = judge(read(), { workload, attended: false });
|
|
312
|
+
verdict = judge(read(), { workload, attended: false, size });
|
|
280
313
|
}
|
|
281
314
|
return { ...verdict, waitedMs: Date.now() - startedAt, source: "formula" };
|
|
282
315
|
};
|
|
283
316
|
|
|
284
|
-
// `memory-room --class toolchain --wait 120 --label NAME` holds a command until there is
|
|
285
|
-
// and always exits 0: a gate that cannot read the sandbox must not be the reason a command
|
|
286
|
-
// the answer instead; `--free` prints the free memory as the daemon counts it, for a caller
|
|
317
|
+
// `memory-room --class toolchain --wait 120 --label NAME [--size targeted] [--pid PID]` holds a command until there is
|
|
318
|
+
// room, printing why it waited, and always exits 0: a gate that cannot read the sandbox must not be the reason a command
|
|
319
|
+
// did not run. `--json` prints the answer instead; `--free` prints the free memory as the daemon counts it, for a caller
|
|
320
|
+
// that sizes itself to it.
|
|
287
321
|
const main = async (args) => {
|
|
288
322
|
const option = (name, fallback) => {
|
|
289
323
|
const at = args.indexOf(`--${name}`);
|
|
@@ -295,7 +329,15 @@ const main = async (args) => {
|
|
|
295
329
|
}
|
|
296
330
|
const waitSeconds = Math.max(0, Number(option("wait", "0")) || 0);
|
|
297
331
|
const label = option("label", "command");
|
|
298
|
-
const
|
|
332
|
+
const pid = Number(option("pid", ""));
|
|
333
|
+
const answer = await askRoom({
|
|
334
|
+
workload: option("class", "toolchain"),
|
|
335
|
+
waitSeconds,
|
|
336
|
+
label,
|
|
337
|
+
intervalMs: Math.max(50, Number(option("interval-ms", "5000")) || 5000),
|
|
338
|
+
size: option("size", undefined) === "targeted" ? "targeted" : undefined,
|
|
339
|
+
pid: Number.isInteger(pid) && pid > 1 ? pid : undefined,
|
|
340
|
+
});
|
|
299
341
|
if (args.includes("--json")) {
|
|
300
342
|
process.stdout.write(`${JSON.stringify(answer)}\n`);
|
|
301
343
|
return;
|