@ricsam/r5d-worker 0.0.81 → 0.0.82
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/cjs/main.cjs +324 -94
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/workspace-command-sync-policy.cjs +3 -3
- package/dist/cjs/workspace-git-sync.cjs +377 -122
- package/dist/cjs/workspace-merge-projection.cjs +392 -0
- package/dist/mjs/main.mjs +322 -94
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/workspace-command-sync-policy.mjs +3 -3
- package/dist/mjs/workspace-git-sync.mjs +381 -122
- package/dist/mjs/workspace-merge-projection.mjs +355 -0
- package/dist/types/main.d.ts +63 -0
- package/dist/types/working-tree-mirror.d.ts +1 -2
- package/dist/types/workspace-command-sync-policy.d.ts +4 -3
- package/dist/types/workspace-git-sync.d.ts +6 -0
- package/dist/types/workspace-merge-projection.d.ts +42 -0
- package/package.json +1 -1
package/dist/cjs/main.cjs
CHANGED
|
@@ -50,6 +50,8 @@ __export(main_exports, {
|
|
|
50
50
|
syncSessionArtifacts: () => syncSessionArtifacts,
|
|
51
51
|
workerChildProcessEnvironment: () => workerChildProcessEnvironment,
|
|
52
52
|
workerGitSecurityTestHarness: () => workerGitSecurityTestHarness,
|
|
53
|
+
workerPtyBridgeTestHarness: () => workerPtyBridgeTestHarness,
|
|
54
|
+
workerPtyTestHarness: () => workerPtyTestHarness,
|
|
53
55
|
writeWorkerTextFile: () => writeWorkerTextFile
|
|
54
56
|
});
|
|
55
57
|
module.exports = __toCommonJS(main_exports);
|
|
@@ -95,6 +97,10 @@ const DEFAULT_READ_MAX_BYTES = 5e4;
|
|
|
95
97
|
const MAX_LINE_LENGTH = 2e3;
|
|
96
98
|
const WORKSPACE_GIT_QUIET_MS = 5e3;
|
|
97
99
|
const WORKSPACE_GIT_PERIODIC_MS = 6e4;
|
|
100
|
+
const PTY_INPUT_BUSY_GRACE_MS = 3e3;
|
|
101
|
+
const PTY_FOREGROUND_POLL_MS = 1e3;
|
|
102
|
+
const PTY_FOREGROUND_IDLE_ENABLED = process.env.R5D_PTY_FOREGROUND_IDLE !== "0";
|
|
103
|
+
const PTY_TMP_PATH_PREFIX = "r5d-worker-tmp://";
|
|
98
104
|
const activeProcesses = /* @__PURE__ */ new Map();
|
|
99
105
|
const credentialBearingProcessGroups = /* @__PURE__ */ new Map();
|
|
100
106
|
const credentialBearingProcessGroupTargets = /* @__PURE__ */ new Map();
|
|
@@ -152,6 +158,106 @@ function assertWorkerChildAdmission(input) {
|
|
|
152
158
|
throw new StaleWorkerAdmissionError();
|
|
153
159
|
}
|
|
154
160
|
}
|
|
161
|
+
function pathIsInsideRoot(rootPath, candidatePath) {
|
|
162
|
+
const relative = import_node_path.default.relative(rootPath, candidatePath);
|
|
163
|
+
return relative === "" || !relative.startsWith(`..${import_node_path.default.sep}`) && relative !== ".." && !import_node_path.default.isAbsolute(relative);
|
|
164
|
+
}
|
|
165
|
+
function resolvePtyEnvFilePath(requestedPath, temporaryRoot = import_node_os.default.tmpdir()) {
|
|
166
|
+
const canonicalTemporaryRoot = import_node_fs.default.realpathSync.native(temporaryRoot);
|
|
167
|
+
if (requestedPath.startsWith(PTY_TMP_PATH_PREFIX)) {
|
|
168
|
+
const filename = requestedPath.slice(PTY_TMP_PATH_PREFIX.length);
|
|
169
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/.test(filename)) {
|
|
170
|
+
throw new Error("PTY environment file temporary token must contain one safe filename");
|
|
171
|
+
}
|
|
172
|
+
return import_node_path.default.join(canonicalTemporaryRoot, filename);
|
|
173
|
+
}
|
|
174
|
+
if (!import_node_path.default.isAbsolute(requestedPath)) {
|
|
175
|
+
throw new Error(`PTY environment file path must be absolute or use ${PTY_TMP_PATH_PREFIX}`);
|
|
176
|
+
}
|
|
177
|
+
const resolvedPath = import_node_path.default.resolve(requestedPath);
|
|
178
|
+
const canonicalParent = import_node_fs.default.realpathSync.native(import_node_path.default.dirname(resolvedPath));
|
|
179
|
+
const canonicalPath = import_node_path.default.join(canonicalParent, import_node_path.default.basename(resolvedPath));
|
|
180
|
+
if (!pathIsInsideRoot(canonicalTemporaryRoot, canonicalPath) || canonicalPath === canonicalTemporaryRoot) {
|
|
181
|
+
throw new Error(`PTY environment file path must be inside ${canonicalTemporaryRoot}`);
|
|
182
|
+
}
|
|
183
|
+
return canonicalPath;
|
|
184
|
+
}
|
|
185
|
+
function removePtyEnvFiles(paths) {
|
|
186
|
+
for (const filePath of paths) {
|
|
187
|
+
try {
|
|
188
|
+
import_node_fs.default.rmSync(filePath, { force: true });
|
|
189
|
+
} catch (error) {
|
|
190
|
+
process.stderr.write(
|
|
191
|
+
`[r5d-worker] failed to remove PTY environment file ${filePath}: ${error instanceof Error ? error.message : String(error)}
|
|
192
|
+
`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function stagePtyEnvFiles(envFiles, temporaryRoot = import_node_os.default.tmpdir()) {
|
|
198
|
+
const staged = { paths: [], resolvedByRequestedPath: /* @__PURE__ */ new Map() };
|
|
199
|
+
try {
|
|
200
|
+
for (const envFile of envFiles ?? []) {
|
|
201
|
+
if (!envFile || typeof envFile.path !== "string" || typeof envFile.content !== "string") {
|
|
202
|
+
throw new Error("PTY environment files require string path and content values");
|
|
203
|
+
}
|
|
204
|
+
if (envFile.mode !== void 0 && envFile.mode !== 384) {
|
|
205
|
+
throw new Error("PTY environment files must use mode 0600");
|
|
206
|
+
}
|
|
207
|
+
const resolvedPath = resolvePtyEnvFilePath(envFile.path, temporaryRoot);
|
|
208
|
+
if (staged.resolvedByRequestedPath.has(envFile.path) || staged.paths.includes(resolvedPath)) {
|
|
209
|
+
throw new Error(`Duplicate PTY environment file path: ${envFile.path}`);
|
|
210
|
+
}
|
|
211
|
+
const descriptor = import_node_fs.default.openSync(
|
|
212
|
+
resolvedPath,
|
|
213
|
+
import_node_fs.default.constants.O_WRONLY | import_node_fs.default.constants.O_CREAT | import_node_fs.default.constants.O_EXCL | (typeof import_node_fs.default.constants.O_NOFOLLOW === "number" ? import_node_fs.default.constants.O_NOFOLLOW : 0),
|
|
214
|
+
384
|
|
215
|
+
);
|
|
216
|
+
staged.paths.push(resolvedPath);
|
|
217
|
+
staged.resolvedByRequestedPath.set(envFile.path, resolvedPath);
|
|
218
|
+
try {
|
|
219
|
+
import_node_fs.default.writeFileSync(descriptor, envFile.content, "utf8");
|
|
220
|
+
import_node_fs.default.fchmodSync(descriptor, 384);
|
|
221
|
+
import_node_fs.default.fsyncSync(descriptor);
|
|
222
|
+
} finally {
|
|
223
|
+
import_node_fs.default.closeSync(descriptor);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return staged;
|
|
227
|
+
} catch (error) {
|
|
228
|
+
removePtyEnvFiles(staged.paths);
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
function resolvePtyEnvFileReferences(env, staged) {
|
|
233
|
+
return Object.fromEntries(Object.entries(env).map(([name, value]) => [name, staged.resolvedByRequestedPath.get(value) ?? value]));
|
|
234
|
+
}
|
|
235
|
+
function workerCommandHasWorkspaceEffect(message) {
|
|
236
|
+
return message.workspaceEffect !== "none";
|
|
237
|
+
}
|
|
238
|
+
function workerPtyIsWorkspaceBusy(pty, now = Date.now(), foregroundIdleEnabled = PTY_FOREGROUND_IDLE_ENABLED) {
|
|
239
|
+
return !foregroundIdleEnabled || pty.foregroundBusy || now - pty.lastInputAt < PTY_INPUT_BUSY_GRACE_MS;
|
|
240
|
+
}
|
|
241
|
+
function parseLinuxPtyForegroundBusy(stat) {
|
|
242
|
+
const commandEnd = stat.lastIndexOf(")");
|
|
243
|
+
if (commandEnd < 0) return null;
|
|
244
|
+
const fields = stat.slice(commandEnd + 1).trim().split(/\s+/);
|
|
245
|
+
const processGroup = Number(fields[2]);
|
|
246
|
+
const foregroundProcessGroup = Number(fields[5]);
|
|
247
|
+
if (!Number.isSafeInteger(processGroup) || processGroup <= 0 || !Number.isSafeInteger(foregroundProcessGroup)) return null;
|
|
248
|
+
return foregroundProcessGroup !== processGroup;
|
|
249
|
+
}
|
|
250
|
+
const workerPtyTestHarness = {
|
|
251
|
+
temporaryPathPrefix: PTY_TMP_PATH_PREFIX,
|
|
252
|
+
inputBusyGraceMs: PTY_INPUT_BUSY_GRACE_MS,
|
|
253
|
+
resolveEnvFilePath: resolvePtyEnvFilePath,
|
|
254
|
+
stageEnvFiles: stagePtyEnvFiles,
|
|
255
|
+
removeEnvFiles: removePtyEnvFiles,
|
|
256
|
+
resolveEnvFileReferences: resolvePtyEnvFileReferences,
|
|
257
|
+
commandHasWorkspaceEffect: workerCommandHasWorkspaceEffect,
|
|
258
|
+
ptyIsWorkspaceBusy: workerPtyIsWorkspaceBusy,
|
|
259
|
+
parseLinuxForegroundBusy: parseLinuxPtyForegroundBusy
|
|
260
|
+
};
|
|
155
261
|
function defaultConfigPath() {
|
|
156
262
|
return import_node_path.default.join(import_node_os.default.homedir(), ".config", "r5d", "r5dctl", "config.json");
|
|
157
263
|
}
|
|
@@ -2333,7 +2439,9 @@ async function executeCommand(input) {
|
|
|
2333
2439
|
});
|
|
2334
2440
|
spawnedProcess = subprocess;
|
|
2335
2441
|
credentialBearingProcessGroups.set(subprocess.pid, subprocess);
|
|
2336
|
-
|
|
2442
|
+
if (workerCommandHasWorkspaceEffect(input.message)) {
|
|
2443
|
+
credentialBearingProcessGroupTargets.set(subprocess.pid, input.resolvedTarget.target);
|
|
2444
|
+
}
|
|
2337
2445
|
activeProcesses.set(input.message.runId, {
|
|
2338
2446
|
process: subprocess,
|
|
2339
2447
|
target: input.resolvedTarget.target,
|
|
@@ -2344,7 +2452,8 @@ async function executeCommand(input) {
|
|
|
2344
2452
|
argv: input.message.argv,
|
|
2345
2453
|
command: input.message.argv.join(" "),
|
|
2346
2454
|
cwd,
|
|
2347
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2455
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2456
|
+
...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {}
|
|
2348
2457
|
});
|
|
2349
2458
|
if (input.message.timeoutMs) {
|
|
2350
2459
|
timeout = setTimeout(() => {
|
|
@@ -2430,7 +2539,9 @@ async function executeStreamingCommand(input) {
|
|
|
2430
2539
|
});
|
|
2431
2540
|
spawnedProcess = subprocess;
|
|
2432
2541
|
credentialBearingProcessGroups.set(subprocess.pid, subprocess);
|
|
2433
|
-
|
|
2542
|
+
if (workerCommandHasWorkspaceEffect(input.message)) {
|
|
2543
|
+
credentialBearingProcessGroupTargets.set(subprocess.pid, input.resolvedTarget.target);
|
|
2544
|
+
}
|
|
2434
2545
|
activeProcesses.set(input.message.runId, {
|
|
2435
2546
|
process: subprocess,
|
|
2436
2547
|
target: input.resolvedTarget.target,
|
|
@@ -2443,7 +2554,8 @@ async function executeStreamingCommand(input) {
|
|
|
2443
2554
|
command: input.message.command,
|
|
2444
2555
|
cwd,
|
|
2445
2556
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2446
|
-
...interactive ? { interactive: true, stdin: subprocess.stdin } : {}
|
|
2557
|
+
...interactive ? { interactive: true, stdin: subprocess.stdin } : {},
|
|
2558
|
+
...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {}
|
|
2447
2559
|
});
|
|
2448
2560
|
started = true;
|
|
2449
2561
|
sendWorkerMessage(input.ws, {
|
|
@@ -2490,7 +2602,8 @@ async function executeStreamingCommand(input) {
|
|
|
2490
2602
|
runId: input.message.runId,
|
|
2491
2603
|
exitCode,
|
|
2492
2604
|
durationMs: Date.now() - startedAt,
|
|
2493
|
-
...timedOut ? { timedOut: true } : {}
|
|
2605
|
+
...timedOut ? { timedOut: true } : {},
|
|
2606
|
+
...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {}
|
|
2494
2607
|
};
|
|
2495
2608
|
pendingProcessTerminals.set(input.message.runId, terminal);
|
|
2496
2609
|
sendWorkerMessage(input.ws, terminal);
|
|
@@ -2502,7 +2615,8 @@ async function executeStreamingCommand(input) {
|
|
|
2502
2615
|
type: "exec_error",
|
|
2503
2616
|
runId: input.message.runId,
|
|
2504
2617
|
error: message,
|
|
2505
|
-
durationMs: Date.now() - startedAt
|
|
2618
|
+
durationMs: Date.now() - startedAt,
|
|
2619
|
+
...input.message.workspaceEffect === "none" ? { workspaceEffect: "none" } : {}
|
|
2506
2620
|
};
|
|
2507
2621
|
pendingProcessTerminals.set(input.message.runId, terminal);
|
|
2508
2622
|
sendWorkerMessage(input.ws, terminal);
|
|
@@ -2583,7 +2697,8 @@ function buildActiveProcessReports() {
|
|
|
2583
2697
|
command: active.command,
|
|
2584
2698
|
...active.cwd ? { cwd: active.cwd } : {},
|
|
2585
2699
|
startedAt: active.startedAt,
|
|
2586
|
-
...active.interactive ? { interactive: true } : {}
|
|
2700
|
+
...active.interactive ? { interactive: true } : {},
|
|
2701
|
+
...active.workspaceEffect === "none" ? { workspaceEffect: "none" } : {}
|
|
2587
2702
|
}));
|
|
2588
2703
|
}
|
|
2589
2704
|
function sendActiveProcessReport(ws) {
|
|
@@ -2595,8 +2710,13 @@ function sendActiveProcessReport(ws) {
|
|
|
2595
2710
|
const PTY_BRIDGE_SCRIPT = String.raw`
|
|
2596
2711
|
const readline = require("node:readline");
|
|
2597
2712
|
const nodePty = require("node-pty");
|
|
2713
|
+
const fs = require("node:fs");
|
|
2714
|
+
const { execFile } = require("node:child_process");
|
|
2598
2715
|
|
|
2599
2716
|
let ptyProcess = null;
|
|
2717
|
+
let foregroundTimer = null;
|
|
2718
|
+
let foregroundPollInFlight = false;
|
|
2719
|
+
let lastForegroundBusy = null;
|
|
2600
2720
|
|
|
2601
2721
|
function send(message, callback) {
|
|
2602
2722
|
process.stdout.write(JSON.stringify(message) + "\n", callback);
|
|
@@ -2606,6 +2726,46 @@ function decode(data) {
|
|
|
2606
2726
|
return Buffer.from(data, "base64").toString("utf8");
|
|
2607
2727
|
}
|
|
2608
2728
|
|
|
2729
|
+
function emitForeground(busy) {
|
|
2730
|
+
if (busy === lastForegroundBusy) return;
|
|
2731
|
+
lastForegroundBusy = busy;
|
|
2732
|
+
send({ type: "foreground", busy });
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
function linuxForegroundBusy(pid) {
|
|
2736
|
+
const stat = fs.readFileSync("/proc/" + pid + "/stat", "utf8");
|
|
2737
|
+
const commandEnd = stat.lastIndexOf(")");
|
|
2738
|
+
if (commandEnd < 0) throw new Error("invalid /proc stat");
|
|
2739
|
+
const fields = stat.slice(commandEnd + 1).trim().split(/\s+/);
|
|
2740
|
+
const processGroup = Number(fields[2]);
|
|
2741
|
+
const foregroundProcessGroup = Number(fields[5]);
|
|
2742
|
+
if (!Number.isSafeInteger(processGroup) || processGroup <= 0 || !Number.isSafeInteger(foregroundProcessGroup)) {
|
|
2743
|
+
throw new Error("invalid process group fields");
|
|
2744
|
+
}
|
|
2745
|
+
return foregroundProcessGroup !== processGroup;
|
|
2746
|
+
}
|
|
2747
|
+
|
|
2748
|
+
function pollForeground() {
|
|
2749
|
+
if (!ptyProcess || !Number.isSafeInteger(ptyProcess.pid) || ptyProcess.pid <= 0) return;
|
|
2750
|
+
if (process.platform === "linux") {
|
|
2751
|
+
try {
|
|
2752
|
+
emitForeground(linuxForegroundBusy(ptyProcess.pid));
|
|
2753
|
+
} catch {
|
|
2754
|
+
emitForeground(true);
|
|
2755
|
+
}
|
|
2756
|
+
return;
|
|
2757
|
+
}
|
|
2758
|
+
if (process.platform !== "darwin" || foregroundPollInFlight) return;
|
|
2759
|
+
foregroundPollInFlight = true;
|
|
2760
|
+
const pid = ptyProcess.pid;
|
|
2761
|
+
execFile("/bin/ps", ["-o", "tpgid=", "-p", String(pid)], (error, stdout) => {
|
|
2762
|
+
foregroundPollInFlight = false;
|
|
2763
|
+
if (!ptyProcess || ptyProcess.pid !== pid) return;
|
|
2764
|
+
const foregroundProcessGroup = Number(String(stdout).trim());
|
|
2765
|
+
emitForeground(Boolean(error) || !Number.isSafeInteger(foregroundProcessGroup) || foregroundProcessGroup !== pid);
|
|
2766
|
+
});
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2609
2769
|
const rl = readline.createInterface({ input: process.stdin });
|
|
2610
2770
|
|
|
2611
2771
|
rl.on("line", (line) => {
|
|
@@ -2624,9 +2784,14 @@ rl.on("line", (line) => {
|
|
|
2624
2784
|
send({ type: "output", data: Buffer.from(data, "utf8").toString("base64") });
|
|
2625
2785
|
});
|
|
2626
2786
|
ptyProcess.onExit((event) => {
|
|
2787
|
+
if (foregroundTimer) clearInterval(foregroundTimer);
|
|
2788
|
+
foregroundTimer = null;
|
|
2627
2789
|
send({ type: "exit", exitCode: event.exitCode, signal: event.signal }, () => process.exit(0));
|
|
2628
2790
|
});
|
|
2629
|
-
send({ type: "opened" });
|
|
2791
|
+
send({ type: "opened", pid: ptyProcess.pid });
|
|
2792
|
+
pollForeground();
|
|
2793
|
+
foregroundTimer = setInterval(pollForeground, ${PTY_FOREGROUND_POLL_MS});
|
|
2794
|
+
foregroundTimer.unref();
|
|
2630
2795
|
return;
|
|
2631
2796
|
}
|
|
2632
2797
|
|
|
@@ -2719,7 +2884,11 @@ function createNodePtyBridge(options) {
|
|
|
2719
2884
|
};
|
|
2720
2885
|
const handleEvent = (event) => {
|
|
2721
2886
|
if (event.type === "opened") {
|
|
2722
|
-
options.onOpened();
|
|
2887
|
+
options.onOpened(event.pid);
|
|
2888
|
+
return;
|
|
2889
|
+
}
|
|
2890
|
+
if (event.type === "foreground") {
|
|
2891
|
+
options.onForeground(event.busy);
|
|
2723
2892
|
return;
|
|
2724
2893
|
}
|
|
2725
2894
|
if (event.type === "output") {
|
|
@@ -2800,6 +2969,9 @@ function createNodePtyBridge(options) {
|
|
|
2800
2969
|
}
|
|
2801
2970
|
};
|
|
2802
2971
|
}
|
|
2972
|
+
const workerPtyBridgeTestHarness = {
|
|
2973
|
+
create: createNodePtyBridge
|
|
2974
|
+
};
|
|
2803
2975
|
function resolveHostShell(command, platform = process.platform) {
|
|
2804
2976
|
if (platform === "win32") {
|
|
2805
2977
|
const file2 = process.env.COMSPEC || "powershell.exe";
|
|
@@ -2835,64 +3007,90 @@ async function openPty(input) {
|
|
|
2835
3007
|
}
|
|
2836
3008
|
});
|
|
2837
3009
|
input.assertAdmission();
|
|
2838
|
-
const
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
}
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
}
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
3010
|
+
const stagedEnvFiles = stagePtyEnvFiles(input.message.envFiles);
|
|
3011
|
+
let envFilesRemoved = false;
|
|
3012
|
+
const cleanupEnvFiles = () => {
|
|
3013
|
+
if (envFilesRemoved) return;
|
|
3014
|
+
envFilesRemoved = true;
|
|
3015
|
+
removePtyEnvFiles(stagedEnvFiles.paths);
|
|
3016
|
+
};
|
|
3017
|
+
try {
|
|
3018
|
+
const ptyProcess = createNodePtyBridge({
|
|
3019
|
+
file: shell.file,
|
|
3020
|
+
args: shell.args,
|
|
3021
|
+
ptyOptions: {
|
|
3022
|
+
name: "xterm-256color",
|
|
3023
|
+
cols: Math.max(1, Math.min(Math.floor(input.message.cols || 80), 500)),
|
|
3024
|
+
rows: Math.max(1, Math.min(Math.floor(input.message.rows || 24), 500)),
|
|
3025
|
+
cwd: input.resolvedTarget.rootPath,
|
|
3026
|
+
env: workerChildProcessEnvironment([
|
|
3027
|
+
githubProcessEnv(),
|
|
3028
|
+
resolvePtyEnvFileReferences(input.message.env ?? {}, stagedEnvFiles),
|
|
3029
|
+
planProcessEnv,
|
|
3030
|
+
targetProcessEnv,
|
|
3031
|
+
shell.env ?? {}
|
|
3032
|
+
])
|
|
3033
|
+
},
|
|
3034
|
+
onOpened: () => {
|
|
3035
|
+
sendWorkerMessage(input.ws, {
|
|
3036
|
+
type: "pty_opened",
|
|
3037
|
+
requestId: input.message.requestId,
|
|
3038
|
+
ptyId: input.message.ptyId
|
|
3039
|
+
});
|
|
3040
|
+
},
|
|
3041
|
+
onForeground: (busy) => {
|
|
3042
|
+
const activePty = activePtys.get(input.message.ptyId);
|
|
3043
|
+
if (activePty && (busy || input.message.command === void 0)) activePty.foregroundBusy = busy;
|
|
3044
|
+
},
|
|
3045
|
+
onOutput: (data) => {
|
|
3046
|
+
outputCoalescer.push(data);
|
|
3047
|
+
},
|
|
3048
|
+
onExit: (event) => {
|
|
3049
|
+
outputCoalescer.flush();
|
|
3050
|
+
input.releaseWorkspaceMutation?.();
|
|
3051
|
+
activePtys.delete(input.message.ptyId);
|
|
3052
|
+
cleanupEnvFiles();
|
|
3053
|
+
input.onTerminal?.();
|
|
3054
|
+
sendWorkerMessage(input.ws, {
|
|
3055
|
+
type: "pty_exit",
|
|
3056
|
+
ptyId: input.message.ptyId,
|
|
3057
|
+
exitCode: event.exitCode,
|
|
3058
|
+
signal: event.signal
|
|
3059
|
+
});
|
|
3060
|
+
},
|
|
3061
|
+
onError: (error) => {
|
|
3062
|
+
outputCoalescer.flush();
|
|
3063
|
+
input.releaseWorkspaceMutation?.();
|
|
3064
|
+
activePtys.delete(input.message.ptyId);
|
|
3065
|
+
cleanupEnvFiles();
|
|
3066
|
+
input.onTerminal?.();
|
|
3067
|
+
sendWorkerMessage(input.ws, {
|
|
3068
|
+
type: "pty_error",
|
|
3069
|
+
requestId: input.message.requestId,
|
|
3070
|
+
ptyId: input.message.ptyId,
|
|
3071
|
+
error: error.message
|
|
3072
|
+
});
|
|
3073
|
+
},
|
|
3074
|
+
onTerminationError: (error) => {
|
|
3075
|
+
sendWorkerMessage(input.ws, {
|
|
3076
|
+
type: "pty_error",
|
|
3077
|
+
requestId: input.message.requestId,
|
|
3078
|
+
ptyId: input.message.ptyId,
|
|
3079
|
+
error: error.message
|
|
3080
|
+
});
|
|
3081
|
+
}
|
|
3082
|
+
});
|
|
3083
|
+
activePtys.set(input.message.ptyId, {
|
|
3084
|
+
...ptyProcess,
|
|
3085
|
+
target: input.resolvedTarget.target,
|
|
3086
|
+
foregroundBusy: true,
|
|
3087
|
+
lastInputAt: 0,
|
|
3088
|
+
...input.releaseWorkspaceMutation ? { releaseWorkspaceMutation: input.releaseWorkspaceMutation } : {}
|
|
3089
|
+
});
|
|
3090
|
+
} catch (error) {
|
|
3091
|
+
cleanupEnvFiles();
|
|
3092
|
+
throw error;
|
|
3093
|
+
}
|
|
2896
3094
|
}
|
|
2897
3095
|
function writePty(ws, message) {
|
|
2898
3096
|
const ptyProcess = activePtys.get(message.ptyId);
|
|
@@ -2904,6 +3102,7 @@ function writePty(ws, message) {
|
|
|
2904
3102
|
});
|
|
2905
3103
|
return;
|
|
2906
3104
|
}
|
|
3105
|
+
ptyProcess.lastInputAt = Date.now();
|
|
2907
3106
|
ptyProcess.write(message.data);
|
|
2908
3107
|
}
|
|
2909
3108
|
function resizePty(message) {
|
|
@@ -3341,13 +3540,24 @@ async function startWorker(options) {
|
|
|
3341
3540
|
});
|
|
3342
3541
|
};
|
|
3343
3542
|
const activeWorkspaceMutationTargets = () => [
|
|
3344
|
-
...Array.from(activeProcesses.values()
|
|
3543
|
+
...Array.from(activeProcesses.values()).filter(workerCommandHasWorkspaceEffect).map(({ target }) => target),
|
|
3345
3544
|
...credentialBearingProcessGroupTargets.values(),
|
|
3346
3545
|
...workspaceSyncPriorityProcessTargets.values(),
|
|
3347
3546
|
...workspaceSyncPriorityOperationTargets.values(),
|
|
3348
|
-
...Array.from(activePtys.values()
|
|
3547
|
+
...Array.from(activePtys.values()).filter((pty) => workerPtyIsWorkspaceBusy(pty)).map(({ target }) => target),
|
|
3349
3548
|
...workspaceSyncPriorityPtyTargets.values()
|
|
3350
3549
|
];
|
|
3550
|
+
let visibleWorkspaceMutationEpoch = 0;
|
|
3551
|
+
const projectWorkspaceMutationEpochs = /* @__PURE__ */ new Map();
|
|
3552
|
+
const recordVisibleWorkspaceMutation = (target) => {
|
|
3553
|
+
if (target.type === "workspace") {
|
|
3554
|
+
if (target.rootProfile === "visible_projects") visibleWorkspaceMutationEpoch += 1;
|
|
3555
|
+
return;
|
|
3556
|
+
}
|
|
3557
|
+
const key = projectBranchKey(target.projectId, target.branchName);
|
|
3558
|
+
projectWorkspaceMutationEpochs.set(key, (projectWorkspaceMutationEpochs.get(key) ?? 0) + 1);
|
|
3559
|
+
};
|
|
3560
|
+
const projectWorkspaceMutationToken = (projectId, branchName) => `${visibleWorkspaceMutationEpoch}:${projectWorkspaceMutationEpochs.get(projectBranchKey(projectId, branchName)) ?? 0}`;
|
|
3351
3561
|
const canonicalWorkspaceMutationIsActive = (targets) => targets.some((target) => target.type === "workspace" && target.rootProfile === "canonical_sync");
|
|
3352
3562
|
const projectBranchMountActivityBusy = (projectId, branchName, branchPath) => {
|
|
3353
3563
|
const activeTargets = activeWorkspaceMutationTargets();
|
|
@@ -3384,6 +3594,7 @@ async function startWorker(options) {
|
|
|
3384
3594
|
preserveLocalOnInitialOuterAbsence: creatorLocalIncarnation,
|
|
3385
3595
|
preserveLocalOnHydrationBasisChange: preservesCheckoutPathMove,
|
|
3386
3596
|
busy: () => projectBranchMountBusy(project.projectId, branch.branchName, branchPath),
|
|
3597
|
+
mutationToken: () => projectWorkspaceMutationToken(project.projectId, branch.branchName),
|
|
3387
3598
|
busyForRecovery: () => projectBranchMountActivityBusy(project.projectId, branch.branchName, branchPath)
|
|
3388
3599
|
});
|
|
3389
3600
|
mounts.push({
|
|
@@ -3397,6 +3608,7 @@ async function startWorker(options) {
|
|
|
3397
3608
|
preserveLocalOnInitialOuterAbsence: creatorLocalIncarnation,
|
|
3398
3609
|
preserveLocalOnHydrationBasisChange: preservesCheckoutPathMove,
|
|
3399
3610
|
busy: () => projectBranchMountBusy(project.projectId, branch.branchName, branchPath),
|
|
3611
|
+
mutationToken: () => projectWorkspaceMutationToken(project.projectId, branch.branchName),
|
|
3400
3612
|
busyForRecovery: () => projectBranchMountActivityBusy(project.projectId, branch.branchName, branchPath)
|
|
3401
3613
|
});
|
|
3402
3614
|
}
|
|
@@ -3417,6 +3629,7 @@ async function startWorker(options) {
|
|
|
3417
3629
|
const activeTargets = activeWorkspaceMutationTargets();
|
|
3418
3630
|
return canonicalWorkspaceMutationIsActive(activeTargets) || (0, import_workspace_automatic_sync_policy.hasActiveVisibleProjectsWorkspaceTarget)(activeTargets);
|
|
3419
3631
|
},
|
|
3632
|
+
mutationToken: () => String(visibleWorkspaceMutationEpoch),
|
|
3420
3633
|
busyForRecovery: () => {
|
|
3421
3634
|
const activeTargets = activeWorkspaceMutationTargets();
|
|
3422
3635
|
return canonicalWorkspaceMutationIsActive(activeTargets) || (0, import_workspace_automatic_sync_policy.hasActiveVisibleProjectsWorkspaceTarget)(activeTargets);
|
|
@@ -3774,6 +3987,7 @@ async function startWorker(options) {
|
|
|
3774
3987
|
localChangesDiscarded: false,
|
|
3775
3988
|
...result.conflictPaths ? { conflictPaths: result.conflictPaths } : {},
|
|
3776
3989
|
...result.conflictSnapshotRefs ? { conflictSnapshotRefs: result.conflictSnapshotRefs } : {},
|
|
3990
|
+
...result.conflictKind ? { conflictKind: result.conflictKind } : {},
|
|
3777
3991
|
...result.error ? { error: result.error } : {}
|
|
3778
3992
|
};
|
|
3779
3993
|
};
|
|
@@ -3840,10 +4054,7 @@ async function startWorker(options) {
|
|
|
3840
4054
|
};
|
|
3841
4055
|
}
|
|
3842
4056
|
const outerRemediation = input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm";
|
|
3843
|
-
const
|
|
3844
|
-
(mount) => !(mount.deleteWhenSourceMissing && !import_node_fs.default.existsSync(mount.sourcePath)) && mount.busy?.()
|
|
3845
|
-
);
|
|
3846
|
-
const observedProjectHeads = outerRemediation || ordinaryCycleHasBusyLiveMount ? [] : observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false });
|
|
4057
|
+
const observedProjectHeads = outerRemediation ? [] : observeProjectHeadsBeforeOuterWorkspace({ allowNonFastForward: false, failClosed: false });
|
|
3847
4058
|
const inboundMoveMountIds = new Set(
|
|
3848
4059
|
observedProjectHeads.flatMap(
|
|
3849
4060
|
({ projectId, observations }) => observations.filter(({ shouldMove }) => shouldMove).map(({ branchName }) => projectMountId(projectId, branchName))
|
|
@@ -3999,7 +4210,8 @@ async function startWorker(options) {
|
|
|
3999
4210
|
);
|
|
4000
4211
|
workspaceAutomaticTimer.unref();
|
|
4001
4212
|
};
|
|
4002
|
-
const markWorkspaceDirty = (trigger, force = false) => {
|
|
4213
|
+
const markWorkspaceDirty = (trigger, force = false, target) => {
|
|
4214
|
+
if (target) recordVisibleWorkspaceMutation(target);
|
|
4003
4215
|
scheduleAutomaticWorkspaceSync(trigger, force ? 0 : WORKSPACE_GIT_QUIET_MS);
|
|
4004
4216
|
};
|
|
4005
4217
|
const targetMayMutateVisibleWorkspace = (target) => target.type === "project" || target.rootProfile === "visible_projects";
|
|
@@ -4260,7 +4472,7 @@ async function startWorker(options) {
|
|
|
4260
4472
|
sourcePath: recoverySourceOverrides.get(mount.id) ?? mount.sourcePath,
|
|
4261
4473
|
busy: mount.busyForRecovery ?? mount.busy
|
|
4262
4474
|
}));
|
|
4263
|
-
(0, import_workspace_git_sync.recoverWorkspaceGitHydration)(workspaceShadowRoot, recoveryMounts);
|
|
4475
|
+
(0, import_workspace_git_sync.recoverWorkspaceGitHydration)(workspaceShadowRoot, recoveryMounts, { preserveStaleBases: true });
|
|
4264
4476
|
for (const project of message.projects) {
|
|
4265
4477
|
for (const { branchName } of project.preserveOnlyBranches) {
|
|
4266
4478
|
if (stillPendingCreatedBranches.has((0, import_workspace_automatic_sync_policy.pendingCreatedBranchKey)(project.projectId, branchName))) {
|
|
@@ -4436,7 +4648,8 @@ async function startWorker(options) {
|
|
|
4436
4648
|
capabilities: {
|
|
4437
4649
|
updateClis: true,
|
|
4438
4650
|
browserPortForwarding: true,
|
|
4439
|
-
execStdinV1: true
|
|
4651
|
+
execStdinV1: true,
|
|
4652
|
+
ptyEnvFilesV1: true
|
|
4440
4653
|
},
|
|
4441
4654
|
projectRoot: projectsRoot,
|
|
4442
4655
|
artifactRoot,
|
|
@@ -4811,6 +5024,9 @@ async function startWorker(options) {
|
|
|
4811
5024
|
});
|
|
4812
5025
|
return;
|
|
4813
5026
|
}
|
|
5027
|
+
if (active.workspaceEffect !== "none" && targetMayMutateVisibleWorkspace(active.target)) {
|
|
5028
|
+
recordVisibleWorkspaceMutation(active.target);
|
|
5029
|
+
}
|
|
4814
5030
|
try {
|
|
4815
5031
|
let bytesWritten = 0;
|
|
4816
5032
|
if (message.data !== void 0 && message.data.length > 0) {
|
|
@@ -4821,8 +5037,8 @@ async function startWorker(options) {
|
|
|
4821
5037
|
await active.stdin.end();
|
|
4822
5038
|
active.stdin = void 0;
|
|
4823
5039
|
}
|
|
4824
|
-
if (targetMayMutateVisibleWorkspace(active.target)) {
|
|
4825
|
-
markWorkspaceDirty({ type: "shell_inline", detail: `exec stdin ${message.runId}` });
|
|
5040
|
+
if (active.workspaceEffect !== "none" && targetMayMutateVisibleWorkspace(active.target)) {
|
|
5041
|
+
markWorkspaceDirty({ type: "shell_inline", detail: `exec stdin ${message.runId}` }, false, active.target);
|
|
4826
5042
|
}
|
|
4827
5043
|
sendAck({
|
|
4828
5044
|
result: {
|
|
@@ -4849,6 +5065,7 @@ async function startWorker(options) {
|
|
|
4849
5065
|
}
|
|
4850
5066
|
await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
|
|
4851
5067
|
workspaceSyncPriorityPtyTargets.set(message.ptyId, message.target);
|
|
5068
|
+
if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
|
|
4852
5069
|
});
|
|
4853
5070
|
let releaseWorkspaceMutation;
|
|
4854
5071
|
let mutationLeaseTransferred = false;
|
|
@@ -4866,7 +5083,7 @@ async function startWorker(options) {
|
|
|
4866
5083
|
...releaseWorkspaceMutation ? { releaseWorkspaceMutation } : {},
|
|
4867
5084
|
...targetMayMutateVisibleWorkspace(message.target) ? {
|
|
4868
5085
|
onTerminal: () => {
|
|
4869
|
-
markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} completed` }, true);
|
|
5086
|
+
markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} completed` }, true, message.target);
|
|
4870
5087
|
}
|
|
4871
5088
|
} : {}
|
|
4872
5089
|
});
|
|
@@ -4887,7 +5104,7 @@ async function startWorker(options) {
|
|
|
4887
5104
|
if (message.type === "pty_input") {
|
|
4888
5105
|
const activePty = activePtys.get(message.ptyId);
|
|
4889
5106
|
if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
|
|
4890
|
-
markWorkspaceDirty({ type: "shell_inline", detail: `pty ${message.ptyId}` });
|
|
5107
|
+
markWorkspaceDirty({ type: "shell_inline", detail: `pty ${message.ptyId}` }, false, activePty.target);
|
|
4891
5108
|
}
|
|
4892
5109
|
writePty(ws, message);
|
|
4893
5110
|
return;
|
|
@@ -4900,7 +5117,7 @@ async function startWorker(options) {
|
|
|
4900
5117
|
const activePty = activePtys.get(message.ptyId);
|
|
4901
5118
|
closePty(message);
|
|
4902
5119
|
if (activePty && targetMayMutateVisibleWorkspace(activePty.target)) {
|
|
4903
|
-
markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} closed` }, true);
|
|
5120
|
+
markWorkspaceDirty({ type: "process_terminal", detail: `pty ${message.ptyId} closed` }, true, activePty.target);
|
|
4904
5121
|
}
|
|
4905
5122
|
return;
|
|
4906
5123
|
}
|
|
@@ -4917,11 +5134,15 @@ async function startWorker(options) {
|
|
|
4917
5134
|
}
|
|
4918
5135
|
let result;
|
|
4919
5136
|
let targetReserved = false;
|
|
5137
|
+
const hasWorkspaceEffect = workerCommandHasWorkspaceEffect(message);
|
|
4920
5138
|
try {
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
5139
|
+
if (hasWorkspaceEffect) {
|
|
5140
|
+
await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
|
|
5141
|
+
workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
|
|
5142
|
+
targetReserved = true;
|
|
5143
|
+
if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
|
|
5144
|
+
});
|
|
5145
|
+
}
|
|
4925
5146
|
const runCommand = async () => {
|
|
4926
5147
|
const resolvedTarget = resolveMessageTarget(message.target);
|
|
4927
5148
|
process.stdout.write(`[r5d-worker] exec ${message.runId}: ${message.argv.join(" ")}
|
|
@@ -4951,10 +5172,11 @@ async function startWorker(options) {
|
|
|
4951
5172
|
if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
|
|
4952
5173
|
}
|
|
4953
5174
|
ws.send(JSON.stringify(result));
|
|
4954
|
-
if (targetMayMutateVisibleWorkspace(message.target)) {
|
|
5175
|
+
if (hasWorkspaceEffect && targetMayMutateVisibleWorkspace(message.target)) {
|
|
4955
5176
|
markWorkspaceDirty(
|
|
4956
5177
|
{ type: "shell_inline", sessionId: message.sessionId, processRunId: message.runId, detail: "foreground command completed" },
|
|
4957
|
-
true
|
|
5178
|
+
true,
|
|
5179
|
+
message.target
|
|
4958
5180
|
);
|
|
4959
5181
|
}
|
|
4960
5182
|
return;
|
|
@@ -4974,6 +5196,7 @@ async function startWorker(options) {
|
|
|
4974
5196
|
requestId: message.requestId,
|
|
4975
5197
|
runId: message.runId
|
|
4976
5198
|
});
|
|
5199
|
+
const hasWorkspaceEffect = workerCommandHasWorkspaceEffect(message);
|
|
4977
5200
|
const runCommand = async () => {
|
|
4978
5201
|
try {
|
|
4979
5202
|
const resolvedTarget = resolveMessageTarget(message.target);
|
|
@@ -4990,7 +5213,7 @@ async function startWorker(options) {
|
|
|
4990
5213
|
assertAdmission: assertMessageAdmission
|
|
4991
5214
|
});
|
|
4992
5215
|
} finally {
|
|
4993
|
-
if (targetMayMutateVisibleWorkspace(message.target)) {
|
|
5216
|
+
if (hasWorkspaceEffect && targetMayMutateVisibleWorkspace(message.target)) {
|
|
4994
5217
|
markWorkspaceDirty(
|
|
4995
5218
|
{
|
|
4996
5219
|
type: "process_terminal",
|
|
@@ -4998,7 +5221,8 @@ async function startWorker(options) {
|
|
|
4998
5221
|
processRunId: message.runId,
|
|
4999
5222
|
detail: "process completed"
|
|
5000
5223
|
},
|
|
5001
|
-
true
|
|
5224
|
+
true,
|
|
5225
|
+
message.target
|
|
5002
5226
|
);
|
|
5003
5227
|
}
|
|
5004
5228
|
}
|
|
@@ -5006,10 +5230,13 @@ async function startWorker(options) {
|
|
|
5006
5230
|
const execution = (async () => {
|
|
5007
5231
|
let targetReserved = false;
|
|
5008
5232
|
try {
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5233
|
+
if (hasWorkspaceEffect) {
|
|
5234
|
+
await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
|
|
5235
|
+
workspaceSyncPriorityProcessTargets.set(message.runId, message.target);
|
|
5236
|
+
targetReserved = true;
|
|
5237
|
+
if (targetMayMutateVisibleWorkspace(message.target)) recordVisibleWorkspaceMutation(message.target);
|
|
5238
|
+
});
|
|
5239
|
+
}
|
|
5013
5240
|
await (0, import_workspace_command_sync_policy.runWorkspaceCommand)(message.target, workspaceSyncSingleFlight, runCommand);
|
|
5014
5241
|
} finally {
|
|
5015
5242
|
if (targetReserved) workspaceSyncPriorityProcessTargets.delete(message.runId);
|
|
@@ -5032,6 +5259,7 @@ async function startWorker(options) {
|
|
|
5032
5259
|
if (reservesVisibleWorkspace) {
|
|
5033
5260
|
await (0, import_workspace_command_sync_policy.reserveWorkspaceCommandAfterCurrentSync)(message.target, workspaceSyncSingleFlight, () => {
|
|
5034
5261
|
workspaceSyncPriorityOperationTargets.set(message.requestId, message.target);
|
|
5262
|
+
if (mutatesVisibleWorkspace) recordVisibleWorkspaceMutation(message.target);
|
|
5035
5263
|
});
|
|
5036
5264
|
}
|
|
5037
5265
|
let dirtyTrigger;
|
|
@@ -5061,7 +5289,7 @@ async function startWorker(options) {
|
|
|
5061
5289
|
toolCallId: message.requestId,
|
|
5062
5290
|
...message.target.type === "project" ? { projectId: message.target.projectId, branchName: message.target.branchName } : {}
|
|
5063
5291
|
};
|
|
5064
|
-
markWorkspaceDirty(dirtyTrigger);
|
|
5292
|
+
markWorkspaceDirty(dirtyTrigger, false, message.target);
|
|
5065
5293
|
}
|
|
5066
5294
|
} catch (error) {
|
|
5067
5295
|
ws.send(
|
|
@@ -5222,5 +5450,7 @@ if (isCliEntrypoint()) {
|
|
|
5222
5450
|
syncSessionArtifacts,
|
|
5223
5451
|
workerChildProcessEnvironment,
|
|
5224
5452
|
workerGitSecurityTestHarness,
|
|
5453
|
+
workerPtyBridgeTestHarness,
|
|
5454
|
+
workerPtyTestHarness,
|
|
5225
5455
|
writeWorkerTextFile
|
|
5226
5456
|
});
|