@mindstudio-ai/remy 0.1.148 → 0.1.150
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/automatedActions/approveInitialPlan.md +14 -0
- package/dist/automatedActions/buildFromInitialSpec.md +5 -3
- package/dist/automatedActions/buildFromRoadmap.md +4 -0
- package/dist/automatedActions/postBuildPolish.md +2 -2
- package/dist/headless.d.ts +95 -2
- package/dist/headless.js +785 -486
- package/dist/index.js +1106 -768
- package/dist/prompt/compiled/design.md +1 -0
- package/dist/prompt/static/authoring.md +0 -20
- package/dist/prompt/static/coding.md +1 -0
- package/dist/prompt/static/instructions.md +1 -1
- package/dist/prompt/static/intake.md +4 -6
- package/dist/prompt/static/team.md +3 -1
- package/dist/subagents/browserAutomation/prompt.md +1 -0
- package/dist/subagents/productVision/pitch-deck-shell.html +37 -7
- package/package.json +1 -1
package/dist/headless.js
CHANGED
|
@@ -4,17 +4,8 @@ var __export = (target, all) => {
|
|
|
4
4
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
5
|
};
|
|
6
6
|
|
|
7
|
-
// src/headless.ts
|
|
7
|
+
// src/headless/index.ts
|
|
8
8
|
import { createInterface } from "readline";
|
|
9
|
-
import {
|
|
10
|
-
writeFileSync,
|
|
11
|
-
readFileSync,
|
|
12
|
-
unlinkSync,
|
|
13
|
-
mkdirSync,
|
|
14
|
-
existsSync
|
|
15
|
-
} from "fs";
|
|
16
|
-
import { writeFile } from "fs/promises";
|
|
17
|
-
import { basename, join, extname } from "path";
|
|
18
9
|
|
|
19
10
|
// src/logger.ts
|
|
20
11
|
import fs from "fs";
|
|
@@ -377,11 +368,11 @@ Old tool results are periodically cleared from the conversation to save context
|
|
|
377
368
|
</conversation_summaries>
|
|
378
369
|
|
|
379
370
|
<project_onboarding>
|
|
380
|
-
New projects progress through
|
|
371
|
+
New projects progress through three onboarding states. The user might skip this entirely and jump straight into working on the existing scaffold (which defaults to onboardingFinished), but ideally new projects move through each phase:
|
|
381
372
|
|
|
382
|
-
- **intake**: Gathering requirements. The project has scaffold code (a "hello world" starter) but it's not the user's app yet. Focus on understanding what they want to build, not on the existing code.
|
|
383
|
-
- **
|
|
384
|
-
- **
|
|
373
|
+
- **intake**: Gathering requirements. The project has scaffold code (a "hello world" starter) but it's not the user's app yet. Focus on understanding what they want to build, not on the existing code. Intake ends with a plan proposal via writePlan.
|
|
374
|
+
- **building**: The user approved the initial plan. The agent is writing the spec and building the app. This can take a while and involves heavy tool use (spec authoring, design expert consultation, code generation, verification, polishing).
|
|
375
|
+
- **buildComplete**: The build is done. The frontend is showing the user a reveal experience (pitch deck, app preview). The agent does not need to do anything in this state; the frontend advances to onboardingFinished when the user is ready.
|
|
385
376
|
- **onboardingFinished**: The project is built and ready. Full development mode with all tools available. From here on, keep spec and code in sync as changes are made.
|
|
386
377
|
</project_onboarding>
|
|
387
378
|
|
|
@@ -1298,7 +1289,7 @@ var writePlanTool = {
|
|
|
1298
1289
|
clearable: false,
|
|
1299
1290
|
definition: {
|
|
1300
1291
|
name: "writePlan",
|
|
1301
|
-
description: "Write an implementation plan for user approval before making changes. Use this only for large, multi-step changes like new features, new interface types, or when the user explicitly asks to see a plan. Most work should be done autonomously without a plan. Write a clear markdown summary of what you intend to do in plain language \u2014 describe the changes from the user's perspective, not as a list of files and code paths. The plan is
|
|
1292
|
+
description: "Write an implementation plan for user approval before making changes. Use this only for large, multi-step changes like new features, new interface types, or when the user explicitly asks to see a plan. Most work should be done autonomously without a plan. Write a clear markdown summary of what you intend to do in plain language \u2014 describe the changes from the user's perspective, not as a list of files and code paths. The plan is displayed standalone in the UI with approve/reject buttons, so write only the plan itself \u2014 no conversational text, no 'what do you think?', no next-steps narration. Say those things in your chat message instead. If the user asks for revisions, call this tool again with updated content to overwrite the plan.",
|
|
1302
1293
|
inputSchema: {
|
|
1303
1294
|
type: "object",
|
|
1304
1295
|
properties: {
|
|
@@ -1368,13 +1359,13 @@ var setProjectOnboardingStateTool = {
|
|
|
1368
1359
|
clearable: false,
|
|
1369
1360
|
definition: {
|
|
1370
1361
|
name: "setProjectOnboardingState",
|
|
1371
|
-
description: "Advance the project onboarding state.
|
|
1362
|
+
description: "Advance the project onboarding state. Only call this when an automated action explicitly instructs you to \u2014 calling it at the wrong time skips stages the user hasn't experienced. Forward-only: building \u2192 buildComplete \u2192 onboardingFinished. `onboardingFinished` is set by the frontend after the user dismisses the reveal; do not call it yourself.",
|
|
1372
1363
|
inputSchema: {
|
|
1373
1364
|
type: "object",
|
|
1374
1365
|
properties: {
|
|
1375
1366
|
state: {
|
|
1376
1367
|
type: "string",
|
|
1377
|
-
enum: ["
|
|
1368
|
+
enum: ["building", "buildComplete", "onboardingFinished"],
|
|
1378
1369
|
description: "The onboarding state to advance to."
|
|
1379
1370
|
}
|
|
1380
1371
|
},
|
|
@@ -2541,7 +2532,7 @@ var runMethodTool = {
|
|
|
2541
2532
|
clearable: true,
|
|
2542
2533
|
definition: {
|
|
2543
2534
|
name: "runMethod",
|
|
2544
|
-
description:
|
|
2535
|
+
description: 'Run a method in the dev environment and return the result. Use for testing methods after writing or modifying them. Returns output, captured console output, errors with stack traces, and duration. If it fails, check .logs/tunnel.log or .logs/requests.ndjson for more details. Returns synchronously \u2014 no need to sleep before checking results.\n\nBy default methods run unauthenticated. If the method is auth-gated (calls `auth.requireRole()`, filters on `auth.userId`, etc.), pass `userId: "testUser"` to run as the default test user \u2014 no scenario setup required, no userId lookup.',
|
|
2545
2536
|
inputSchema: {
|
|
2546
2537
|
type: "object",
|
|
2547
2538
|
properties: {
|
|
@@ -2553,14 +2544,14 @@ var runMethodTool = {
|
|
|
2553
2544
|
type: "object",
|
|
2554
2545
|
description: "The input payload to pass to the method. Omit for methods that take no input."
|
|
2555
2546
|
},
|
|
2547
|
+
userId: {
|
|
2548
|
+
type: "string",
|
|
2549
|
+
description: 'Optional. Run the method as a specific user. Pass "testUser" to auto-auth as the default test user (the sandbox handles user creation/lookup \u2014 no scenario setup needed). Or pass a real user ID from scenario-seeded data for a specific user. Overrides session-level impersonation for this call only.'
|
|
2550
|
+
},
|
|
2556
2551
|
roles: {
|
|
2557
2552
|
type: "array",
|
|
2558
2553
|
items: { type: "string" },
|
|
2559
|
-
description: 'Optional. Role names for this request (e.g. ["admin"]).
|
|
2560
|
-
},
|
|
2561
|
-
userId: {
|
|
2562
|
-
type: "string",
|
|
2563
|
-
description: "Optional. User ID for this request \u2014 use a managed user's ID to simulate their identity. Overrides session-level impersonation for this call only."
|
|
2554
|
+
description: 'Optional. Role names for this request (e.g. ["admin"]). Combine with `userId` to test a specific role, or use alone to test role-gated logic without a full identity. Overrides session-level impersonation for this call only.'
|
|
2564
2555
|
}
|
|
2565
2556
|
},
|
|
2566
2557
|
required: ["method"]
|
|
@@ -2608,10 +2599,10 @@ async function analyzeImage(params) {
|
|
|
2608
2599
|
}
|
|
2609
2600
|
|
|
2610
2601
|
// src/tools/_helpers/screenshot.ts
|
|
2611
|
-
var SCREENSHOT_ANALYSIS_PROMPT = `Describe everything visible on screen from top to bottom \u2014 every element, its position, its size relative to the viewport, its colors, its content. Be comprehensive, thorough, and spatial. After the inventory, note anything that looks visually broken (overlapping elements, clipped text, misaligned components)
|
|
2602
|
+
var SCREENSHOT_ANALYSIS_PROMPT = `Describe everything visible on screen from top to bottom \u2014 every element, its position, its size relative to the viewport, its colors, its content. Be comprehensive, thorough, and spatial. After the inventory, note anything that looks visually broken (overlapping elements, clipped text, misaligned components).`;
|
|
2603
|
+
var TEXT_WRAP_DISCLAIMER = `Note: ignore text wrapping issues. Screenshots occasionally show text wrapping onto an extra line compared to the live page \u2014 most noticeable in buttons, badges, and headings. This is a known limitation of SVG foreignObject rendering used the DOM-to-image capture library that took the screenshot. The browser's SVG renderer computes slightly wider text metrics than the HTML layout engine, so text that fits on one line in the live DOM can overflow by a fraction of a pixel in the capture - this is not a real issue.
|
|
2612
2604
|
|
|
2613
2605
|
Respond only with your analysis as Markdown and absolutely no other text. Do not use emojis - use unicode if you need symbols.`;
|
|
2614
|
-
var TEXT_WRAP_DISCLAIMER = `Note: ignore text wrapping issues. Screenshots occasionally show text wrapping onto an extra line compared to the live page \u2014 most noticeable in buttons, badges, and headings. This is a known limitation of SVG foreignObject rendering used the DOM-to-image capture library that took the screenshot. The browser's SVG renderer computes slightly wider text metrics than the HTML layout engine, so text that fits on one line in the live DOM can overflow by a fraction of a pixel in the capture - this is not a real issue.`;
|
|
2615
2606
|
function buildScreenshotAnalysisPrompt(opts) {
|
|
2616
2607
|
let p = opts?.prompt || SCREENSHOT_ANALYSIS_PROMPT;
|
|
2617
2608
|
if (opts?.styleMap) {
|
|
@@ -2695,23 +2686,22 @@ async function checkBrowserConnected() {
|
|
|
2695
2686
|
if (!status.connected) {
|
|
2696
2687
|
return {
|
|
2697
2688
|
connected: false,
|
|
2698
|
-
|
|
2689
|
+
reason: BROWSER_UNAVAILABLE_MESSAGE
|
|
2699
2690
|
};
|
|
2700
2691
|
}
|
|
2701
2692
|
return { connected: true };
|
|
2702
|
-
} catch
|
|
2693
|
+
} catch {
|
|
2703
2694
|
return {
|
|
2704
2695
|
connected: false,
|
|
2705
|
-
|
|
2696
|
+
reason: BROWSER_UNAVAILABLE_MESSAGE
|
|
2706
2697
|
};
|
|
2707
2698
|
}
|
|
2708
2699
|
}
|
|
2700
|
+
var BROWSER_UNAVAILABLE_MESSAGE = "Browser preview unavailable \u2014 the user has closed their browser and we are continuing to work in the background. This is not a code failure and not something to diagnose. Do not tell the user to click or open anything. Skip the visual check and verify your work through other means: runMethod for backend behavior, queryDatabase for data checks, .logs/devServer.ndjson for build errors, .logs/browser.ndjson for runtime errors, lspDiagnostics for type/syntax, or read the code directly.";
|
|
2709
2701
|
|
|
2710
2702
|
// src/statusWatcher.ts
|
|
2711
2703
|
function startStatusWatcher(config) {
|
|
2712
|
-
const { apiConfig, getContext, onStatus, interval =
|
|
2713
|
-
let lastLabel = "";
|
|
2714
|
-
let lastContext = "";
|
|
2704
|
+
const { apiConfig, getContext, onStatus, interval = 5e3, signal } = config;
|
|
2715
2705
|
let inflight = false;
|
|
2716
2706
|
let stopped = false;
|
|
2717
2707
|
const url = `${apiConfig.baseUrl}/_internal/v2/agent/remy/generate-status`;
|
|
@@ -2722,10 +2712,9 @@ function startStatusWatcher(config) {
|
|
|
2722
2712
|
inflight = true;
|
|
2723
2713
|
try {
|
|
2724
2714
|
const context = getContext();
|
|
2725
|
-
if (!context
|
|
2715
|
+
if (!context) {
|
|
2726
2716
|
return;
|
|
2727
2717
|
}
|
|
2728
|
-
lastContext = context;
|
|
2729
2718
|
const res = await fetch(url, {
|
|
2730
2719
|
method: "POST",
|
|
2731
2720
|
headers: {
|
|
@@ -2735,15 +2724,11 @@ function startStatusWatcher(config) {
|
|
|
2735
2724
|
body: JSON.stringify({ context }),
|
|
2736
2725
|
signal
|
|
2737
2726
|
});
|
|
2738
|
-
if (!res.ok) {
|
|
2727
|
+
if (!res.ok || stopped) {
|
|
2739
2728
|
return;
|
|
2740
2729
|
}
|
|
2741
2730
|
const data = await res.json();
|
|
2742
|
-
if (!data.label
|
|
2743
|
-
return;
|
|
2744
|
-
}
|
|
2745
|
-
lastLabel = data.label;
|
|
2746
|
-
if (stopped) {
|
|
2731
|
+
if (!data.label) {
|
|
2747
2732
|
return;
|
|
2748
2733
|
}
|
|
2749
2734
|
onStatus(data.label);
|
|
@@ -2763,6 +2748,54 @@ function startStatusWatcher(config) {
|
|
|
2763
2748
|
};
|
|
2764
2749
|
}
|
|
2765
2750
|
|
|
2751
|
+
// src/automatedActions/sentinel.ts
|
|
2752
|
+
function sentinel(name) {
|
|
2753
|
+
return `@@automated::${name}@@`;
|
|
2754
|
+
}
|
|
2755
|
+
function automatedMessage(name, body) {
|
|
2756
|
+
return body ? `${sentinel(name)}
|
|
2757
|
+
${body}` : sentinel(name);
|
|
2758
|
+
}
|
|
2759
|
+
function hasSentinel(text, name) {
|
|
2760
|
+
return text.startsWith(sentinel(name));
|
|
2761
|
+
}
|
|
2762
|
+
function isAutomatedMessage(text) {
|
|
2763
|
+
return text.startsWith("@@automated::");
|
|
2764
|
+
}
|
|
2765
|
+
function parseSentinel(text) {
|
|
2766
|
+
const match = text.match(/^@@automated::(\w+)@@(.*)/s);
|
|
2767
|
+
if (!match) {
|
|
2768
|
+
return null;
|
|
2769
|
+
}
|
|
2770
|
+
return { name: match[1], remainder: match[2] };
|
|
2771
|
+
}
|
|
2772
|
+
function stripSentinelLine(text) {
|
|
2773
|
+
return text.replace(/^@@automated::[^@]*@@[^\n]*\n?/, "");
|
|
2774
|
+
}
|
|
2775
|
+
function buildBackgroundResultsMessage(results) {
|
|
2776
|
+
const xml = results.map(
|
|
2777
|
+
(r) => `<tool_result id="${r.toolCallId}" name="${r.name}">
|
|
2778
|
+
${r.result}
|
|
2779
|
+
</tool_result>`
|
|
2780
|
+
).join("\n\n");
|
|
2781
|
+
const plural = results.length > 1 ? "s" : "";
|
|
2782
|
+
const body = `This is an automated message containing the result${plural} of ${results.length > 1 ? "tool calls" : "a tool call"} that ${results.length > 1 ? "have" : "has"} been working in the background. This is not a direct message from the user.
|
|
2783
|
+
<background_results>
|
|
2784
|
+
${xml}
|
|
2785
|
+
</background_results>`;
|
|
2786
|
+
return automatedMessage("background_results", body);
|
|
2787
|
+
}
|
|
2788
|
+
function mergeBackgroundResultsMessages(messages) {
|
|
2789
|
+
const results = [];
|
|
2790
|
+
const toolRe = /<tool_result id="([^"]+)" name="([^"]+)">\n([\s\S]*?)\n<\/tool_result>/g;
|
|
2791
|
+
for (const msg of messages) {
|
|
2792
|
+
for (const m of msg.matchAll(toolRe)) {
|
|
2793
|
+
results.push({ toolCallId: m[1], name: m[2], result: m[3] });
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
return buildBackgroundResultsMessage(results);
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2766
2799
|
// src/subagents/common/cleanMessages.ts
|
|
2767
2800
|
function findLastSummaryCheckpoint(messages, name) {
|
|
2768
2801
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
@@ -2856,11 +2889,8 @@ ${summaryBlock.text}
|
|
|
2856
2889
|
}
|
|
2857
2890
|
return true;
|
|
2858
2891
|
}).map((msg) => {
|
|
2859
|
-
if (msg.role === "user" && typeof msg.content === "string" && msg.content
|
|
2860
|
-
return {
|
|
2861
|
-
...msg,
|
|
2862
|
-
content: msg.content.replace(/^@@automated::[^@]*@@[^\n]*\n?/, "")
|
|
2863
|
-
};
|
|
2892
|
+
if (msg.role === "user" && typeof msg.content === "string" && isAutomatedMessage(msg.content)) {
|
|
2893
|
+
return { ...msg, content: stripSentinelLine(msg.content) };
|
|
2864
2894
|
}
|
|
2865
2895
|
if (!Array.isArray(msg.content)) {
|
|
2866
2896
|
return msg;
|
|
@@ -2918,7 +2948,7 @@ async function runSubAgent(config) {
|
|
|
2918
2948
|
const agentName = subAgentId || "sub-agent";
|
|
2919
2949
|
const runStart = Date.now();
|
|
2920
2950
|
log5.info("Sub-agent started", { requestId, parentToolId, agentName });
|
|
2921
|
-
const
|
|
2951
|
+
const emit = (e) => {
|
|
2922
2952
|
onEvent({ ...e, parentToolId });
|
|
2923
2953
|
};
|
|
2924
2954
|
const dateStr = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", {
|
|
@@ -2983,7 +3013,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
2983
3013
|
}
|
|
2984
3014
|
return parts.join("\n");
|
|
2985
3015
|
},
|
|
2986
|
-
onStatus: (label) =>
|
|
3016
|
+
onStatus: (label) => emit({ type: "status", message: label }),
|
|
2987
3017
|
signal
|
|
2988
3018
|
});
|
|
2989
3019
|
try {
|
|
@@ -3000,7 +3030,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
3000
3030
|
signal
|
|
3001
3031
|
},
|
|
3002
3032
|
{
|
|
3003
|
-
onRetry: (attempt) =>
|
|
3033
|
+
onRetry: (attempt) => emit({
|
|
3004
3034
|
type: "status",
|
|
3005
3035
|
message: `Lost connection, retrying (attempt ${attempt + 2} of 3)...`
|
|
3006
3036
|
})
|
|
@@ -3021,14 +3051,14 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
3021
3051
|
startedAt: event.ts
|
|
3022
3052
|
});
|
|
3023
3053
|
}
|
|
3024
|
-
|
|
3054
|
+
emit({ type: "text", text: event.text });
|
|
3025
3055
|
break;
|
|
3026
3056
|
}
|
|
3027
3057
|
case "thinking":
|
|
3028
3058
|
if (!thinkingStartedAt) {
|
|
3029
3059
|
thinkingStartedAt = event.ts;
|
|
3030
3060
|
}
|
|
3031
|
-
|
|
3061
|
+
emit({ type: "thinking", text: event.text });
|
|
3032
3062
|
break;
|
|
3033
3063
|
case "thinking_complete":
|
|
3034
3064
|
contentBlocks.push({
|
|
@@ -3048,7 +3078,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
3048
3078
|
input: event.input,
|
|
3049
3079
|
startedAt: Date.now()
|
|
3050
3080
|
});
|
|
3051
|
-
|
|
3081
|
+
emit({
|
|
3052
3082
|
type: "tool_start",
|
|
3053
3083
|
id: event.id,
|
|
3054
3084
|
name: event.name,
|
|
@@ -3125,7 +3155,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
3125
3155
|
if (externalTools.has(tc.name) && resolveExternalTool) {
|
|
3126
3156
|
result = await resolveExternalTool(tc.id, tc.name, input);
|
|
3127
3157
|
} else {
|
|
3128
|
-
const onLog = (line) =>
|
|
3158
|
+
const onLog = (line) => emit({
|
|
3129
3159
|
type: "tool_input_delta",
|
|
3130
3160
|
id: tc.id,
|
|
3131
3161
|
name: tc.name,
|
|
@@ -3176,7 +3206,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
3176
3206
|
durationMs: Date.now() - toolStart,
|
|
3177
3207
|
isError: r.isError
|
|
3178
3208
|
});
|
|
3179
|
-
|
|
3209
|
+
emit({
|
|
3180
3210
|
type: "tool_done",
|
|
3181
3211
|
id: tc.id,
|
|
3182
3212
|
name: tc.name,
|
|
@@ -3445,7 +3475,7 @@ var browserAutomationTool = {
|
|
|
3445
3475
|
try {
|
|
3446
3476
|
const browserStatus = await checkBrowserConnected();
|
|
3447
3477
|
if (!browserStatus.connected) {
|
|
3448
|
-
return
|
|
3478
|
+
return browserStatus.reason ?? "Browser preview unavailable.";
|
|
3449
3479
|
}
|
|
3450
3480
|
try {
|
|
3451
3481
|
await sidecarRequest("/reset-browser", {}, { timeout: 5e3 });
|
|
@@ -3631,7 +3661,7 @@ var screenshotTool = {
|
|
|
3631
3661
|
try {
|
|
3632
3662
|
const browserStatus = await checkBrowserConnected();
|
|
3633
3663
|
if (!browserStatus.connected) {
|
|
3634
|
-
return
|
|
3664
|
+
return browserStatus.reason ?? "Browser preview unavailable.";
|
|
3635
3665
|
}
|
|
3636
3666
|
return await captureAndAnalyzeScreenshot({
|
|
3637
3667
|
prompt: input.prompt,
|
|
@@ -3969,7 +3999,7 @@ async function execute5(input, onLog, context) {
|
|
|
3969
3999
|
try {
|
|
3970
4000
|
const browserStatus = await checkBrowserConnected();
|
|
3971
4001
|
if (!browserStatus.connected) {
|
|
3972
|
-
return
|
|
4002
|
+
return browserStatus.reason ?? "Browser preview unavailable.";
|
|
3973
4003
|
}
|
|
3974
4004
|
return await captureAndAnalyzeScreenshot({
|
|
3975
4005
|
prompt: input.prompt,
|
|
@@ -4402,7 +4432,7 @@ Each interface type invokes the same backend methods. Methods don't know which i
|
|
|
4402
4432
|
TypeScript running in a sandboxed environment. Any npm package can be installed. Key capabilities:
|
|
4403
4433
|
|
|
4404
4434
|
- Managed SQLite database with typed schemas and automatic migrations. Define a TypeScript interface, push, and the platform handles diffing and migrating.
|
|
4405
|
-
- Built-in app-managed auth. Opt-in via manifest \u2014 developer builds login UI, platform handles verification codes (email
|
|
4435
|
+
- Built-in app-managed auth. Opt-in via manifest \u2014 developer builds login UI, platform handles verification codes (email-code, sms-code) and cookie sessions. API key auth for programmatic access. No OAuth, no social login (no Apple, Google, Facebook, or GitHub sign-in). Backend methods use auth.requireRole() for access control.
|
|
4406
4436
|
- Encrypted secrets with separate dev/prod values, injected as process.env. For third-party service credentials not covered by the SDK.
|
|
4407
4437
|
- Git-native deployment. Push to default branch to deploy.
|
|
4408
4438
|
|
|
@@ -4834,7 +4864,7 @@ Use <current_deck> as your starting point and replace or update the content as n
|
|
|
4834
4864
|
- The deck must be a single HTML file \u2014 it will be rendered in an iframe.
|
|
4835
4865
|
- Must look beautiful on desktop and mobile.
|
|
4836
4866
|
- Animation between slides must be seamless, no flicker or flashing. For reveal animations: hide elements with CSS \`opacity: 0\` only (no transform in CSS). Let GSAP handle transforms via inline styles and never use \`clearProps\`. Use the existing scaffold, do not write your own transition logic or slide mechanics.
|
|
4837
|
-
- Be bold and impactful.
|
|
4867
|
+
- Be bold and impactful. Use images from the spec or generate new images when needed.
|
|
4838
4868
|
- Code must be clean, bug free, and easy to parse. Use GSAP for animations. Pay close attention to layout and alignment to make sure everything is perfect.
|
|
4839
4869
|
- Keep the progress bar and edge chevrons from the shell \u2014 they are part of the navigation UX.
|
|
4840
4870
|
|
|
@@ -5532,20 +5562,23 @@ async function runTurn(params) {
|
|
|
5532
5562
|
parts.push(`Tool: ${toolName}`);
|
|
5533
5563
|
}
|
|
5534
5564
|
if (lastCompletedInput) {
|
|
5535
|
-
parts.push(`Tool input: ${lastCompletedInput.slice(-
|
|
5565
|
+
parts.push(`Tool input: ${lastCompletedInput.slice(-1500)}`);
|
|
5536
5566
|
}
|
|
5537
5567
|
if (lastCompletedResult) {
|
|
5538
|
-
parts.push(`Tool result: ${lastCompletedResult.slice(-
|
|
5568
|
+
parts.push(`Tool result: ${lastCompletedResult.slice(-1500)}`);
|
|
5539
5569
|
}
|
|
5540
|
-
const text = subAgentText || getTextContent(contentBlocks).slice(-
|
|
5570
|
+
const text = subAgentText || getTextContent(contentBlocks).slice(-2e3);
|
|
5541
5571
|
if (text) {
|
|
5542
5572
|
parts.push(`Assistant text: ${text}`);
|
|
5543
5573
|
}
|
|
5544
5574
|
if (onboardingState && onboardingState !== "onboardingFinished") {
|
|
5545
5575
|
parts.push(`Build phase: ${onboardingState}`);
|
|
5546
5576
|
}
|
|
5547
|
-
|
|
5548
|
-
|
|
5577
|
+
const automated = parseSentinel(userMessage);
|
|
5578
|
+
if (automated) {
|
|
5579
|
+
parts.push(`Automated action: ${automated.name}`);
|
|
5580
|
+
} else if (userMessage) {
|
|
5581
|
+
parts.push(`User request: ${userMessage.slice(-500)}`);
|
|
5549
5582
|
}
|
|
5550
5583
|
return parts.join("\n");
|
|
5551
5584
|
},
|
|
@@ -6005,19 +6038,246 @@ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
|
|
|
6005
6038
|
}
|
|
6006
6039
|
};
|
|
6007
6040
|
|
|
6041
|
+
// src/headless/attachments.ts
|
|
6042
|
+
import { mkdirSync, existsSync } from "fs";
|
|
6043
|
+
import { writeFile } from "fs/promises";
|
|
6044
|
+
import { basename, join, extname } from "path";
|
|
6045
|
+
var log11 = createLogger("headless:attachments");
|
|
6046
|
+
var UPLOADS_DIR = "src/.user-uploads";
|
|
6047
|
+
function filenameFromUrl(url) {
|
|
6048
|
+
try {
|
|
6049
|
+
const pathname = new URL(url).pathname;
|
|
6050
|
+
const name = basename(pathname);
|
|
6051
|
+
return name && name !== "/" ? decodeURIComponent(name) : `upload-${Date.now()}`;
|
|
6052
|
+
} catch {
|
|
6053
|
+
return `upload-${Date.now()}`;
|
|
6054
|
+
}
|
|
6055
|
+
}
|
|
6056
|
+
function resolveUniqueFilename(name) {
|
|
6057
|
+
if (!existsSync(join(UPLOADS_DIR, name))) {
|
|
6058
|
+
return name;
|
|
6059
|
+
}
|
|
6060
|
+
const ext = extname(name);
|
|
6061
|
+
const base = name.slice(0, name.length - ext.length);
|
|
6062
|
+
let counter = 1;
|
|
6063
|
+
while (existsSync(join(UPLOADS_DIR, `${base}-${counter}${ext}`))) {
|
|
6064
|
+
counter++;
|
|
6065
|
+
}
|
|
6066
|
+
return `${base}-${counter}${ext}`;
|
|
6067
|
+
}
|
|
6068
|
+
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
6069
|
+
".png",
|
|
6070
|
+
".jpg",
|
|
6071
|
+
".jpeg",
|
|
6072
|
+
".gif",
|
|
6073
|
+
".webp",
|
|
6074
|
+
".svg",
|
|
6075
|
+
".bmp",
|
|
6076
|
+
".ico",
|
|
6077
|
+
".tiff",
|
|
6078
|
+
".tif",
|
|
6079
|
+
".avif",
|
|
6080
|
+
".heic",
|
|
6081
|
+
".heif"
|
|
6082
|
+
]);
|
|
6083
|
+
function isImageAttachment(att) {
|
|
6084
|
+
const name = att.filename || filenameFromUrl(att.url);
|
|
6085
|
+
return IMAGE_EXTENSIONS.has(extname(name).toLowerCase());
|
|
6086
|
+
}
|
|
6087
|
+
async function persistAttachments(attachments) {
|
|
6088
|
+
const nonVoice = attachments.filter((a) => !a.isVoice);
|
|
6089
|
+
if (nonVoice.length === 0) {
|
|
6090
|
+
return { documents: [], images: [] };
|
|
6091
|
+
}
|
|
6092
|
+
mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
6093
|
+
const results = await Promise.allSettled(
|
|
6094
|
+
nonVoice.map(async (att) => {
|
|
6095
|
+
const name = resolveUniqueFilename(
|
|
6096
|
+
att.filename || filenameFromUrl(att.url)
|
|
6097
|
+
);
|
|
6098
|
+
const localPath = join(UPLOADS_DIR, name);
|
|
6099
|
+
const res = await fetch(att.url, {
|
|
6100
|
+
signal: AbortSignal.timeout(3e4)
|
|
6101
|
+
});
|
|
6102
|
+
if (!res.ok) {
|
|
6103
|
+
throw new Error(`HTTP ${res.status} downloading ${att.url}`);
|
|
6104
|
+
}
|
|
6105
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
6106
|
+
await writeFile(localPath, buffer);
|
|
6107
|
+
log11.info("Attachment saved", {
|
|
6108
|
+
filename: name,
|
|
6109
|
+
path: localPath,
|
|
6110
|
+
bytes: buffer.length
|
|
6111
|
+
});
|
|
6112
|
+
let extractedTextPath;
|
|
6113
|
+
if (att.extractedTextUrl) {
|
|
6114
|
+
try {
|
|
6115
|
+
const textRes = await fetch(att.extractedTextUrl, {
|
|
6116
|
+
signal: AbortSignal.timeout(3e4)
|
|
6117
|
+
});
|
|
6118
|
+
if (textRes.ok) {
|
|
6119
|
+
extractedTextPath = `${localPath}.txt`;
|
|
6120
|
+
await writeFile(extractedTextPath, await textRes.text(), "utf-8");
|
|
6121
|
+
log11.info("Extracted text saved", { path: extractedTextPath });
|
|
6122
|
+
}
|
|
6123
|
+
} catch {
|
|
6124
|
+
}
|
|
6125
|
+
}
|
|
6126
|
+
return {
|
|
6127
|
+
filename: name,
|
|
6128
|
+
localPath,
|
|
6129
|
+
remoteUrl: att.url,
|
|
6130
|
+
extractedTextPath
|
|
6131
|
+
};
|
|
6132
|
+
})
|
|
6133
|
+
);
|
|
6134
|
+
const settled = results.map((r, i) => ({
|
|
6135
|
+
result: r.status === "fulfilled" ? r.value : null,
|
|
6136
|
+
isImage: isImageAttachment(nonVoice[i])
|
|
6137
|
+
}));
|
|
6138
|
+
return {
|
|
6139
|
+
documents: settled.filter((s) => !s.isImage).map((s) => s.result),
|
|
6140
|
+
images: settled.filter((s) => s.isImage).map((s) => s.result)
|
|
6141
|
+
};
|
|
6142
|
+
}
|
|
6143
|
+
function buildUploadHeader(results) {
|
|
6144
|
+
const succeeded = results.filter(Boolean);
|
|
6145
|
+
if (succeeded.length === 0) {
|
|
6146
|
+
return "";
|
|
6147
|
+
}
|
|
6148
|
+
if (succeeded.length === 1) {
|
|
6149
|
+
const r = succeeded[0];
|
|
6150
|
+
const parts = [`[Uploaded file: ${r.localPath} (CDN: ${r.remoteUrl})`];
|
|
6151
|
+
if (r.extractedTextPath) {
|
|
6152
|
+
parts.push(`extracted text: ${r.extractedTextPath}`);
|
|
6153
|
+
}
|
|
6154
|
+
return parts.join(" \u2014 ") + "]";
|
|
6155
|
+
}
|
|
6156
|
+
const lines = succeeded.map((r) => {
|
|
6157
|
+
const parts = [`- ${r.localPath} (CDN: ${r.remoteUrl})`];
|
|
6158
|
+
if (r.extractedTextPath) {
|
|
6159
|
+
parts.push(` extracted text: ${r.extractedTextPath}`);
|
|
6160
|
+
}
|
|
6161
|
+
return parts.join("\n");
|
|
6162
|
+
});
|
|
6163
|
+
return `[Uploaded files]
|
|
6164
|
+
${lines.join("\n")}`;
|
|
6165
|
+
}
|
|
6166
|
+
|
|
6167
|
+
// src/headless/planFile.ts
|
|
6168
|
+
import { readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
6169
|
+
var PLAN_FILE3 = ".remy-plan.md";
|
|
6170
|
+
function applyPlanFileSideEffect(rawText) {
|
|
6171
|
+
if (hasSentinel(rawText, "approvePlan") || hasSentinel(rawText, "approveInitialPlan")) {
|
|
6172
|
+
try {
|
|
6173
|
+
const plan = readFileSync(PLAN_FILE3, "utf-8");
|
|
6174
|
+
writeFileSync(
|
|
6175
|
+
PLAN_FILE3,
|
|
6176
|
+
plan.replace(/^status:\s*pending/m, "status: approved"),
|
|
6177
|
+
"utf-8"
|
|
6178
|
+
);
|
|
6179
|
+
} catch {
|
|
6180
|
+
}
|
|
6181
|
+
} else if (hasSentinel(rawText, "rejectPlan")) {
|
|
6182
|
+
try {
|
|
6183
|
+
unlinkSync(PLAN_FILE3);
|
|
6184
|
+
} catch {
|
|
6185
|
+
}
|
|
6186
|
+
}
|
|
6187
|
+
}
|
|
6188
|
+
|
|
6189
|
+
// src/headless/stats.ts
|
|
6190
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
6191
|
+
var STATS_FILE = ".remy-stats.json";
|
|
6192
|
+
function createSessionStats() {
|
|
6193
|
+
return {
|
|
6194
|
+
messageCount: 0,
|
|
6195
|
+
turns: 0,
|
|
6196
|
+
totalInputTokens: 0,
|
|
6197
|
+
totalOutputTokens: 0,
|
|
6198
|
+
totalCacheCreationTokens: 0,
|
|
6199
|
+
totalCacheReadTokens: 0,
|
|
6200
|
+
lastContextSize: 0,
|
|
6201
|
+
compactionInProgress: false,
|
|
6202
|
+
updatedAt: 0
|
|
6203
|
+
};
|
|
6204
|
+
}
|
|
6205
|
+
function loadQueue() {
|
|
6206
|
+
try {
|
|
6207
|
+
const stats = JSON.parse(readFileSync2(STATS_FILE, "utf-8"));
|
|
6208
|
+
if (Array.isArray(stats.queue)) {
|
|
6209
|
+
return stats.queue;
|
|
6210
|
+
}
|
|
6211
|
+
} catch {
|
|
6212
|
+
}
|
|
6213
|
+
return [];
|
|
6214
|
+
}
|
|
6215
|
+
function writeStats(stats, queue) {
|
|
6216
|
+
try {
|
|
6217
|
+
writeFileSync2(
|
|
6218
|
+
STATS_FILE,
|
|
6219
|
+
JSON.stringify({
|
|
6220
|
+
...stats,
|
|
6221
|
+
queue
|
|
6222
|
+
})
|
|
6223
|
+
);
|
|
6224
|
+
} catch {
|
|
6225
|
+
}
|
|
6226
|
+
}
|
|
6227
|
+
|
|
6228
|
+
// src/headless/messageQueue.ts
|
|
6229
|
+
var MessageQueue = class {
|
|
6230
|
+
items = [];
|
|
6231
|
+
onChange;
|
|
6232
|
+
constructor(initial = [], onChange) {
|
|
6233
|
+
this.items = [...initial];
|
|
6234
|
+
this.onChange = onChange;
|
|
6235
|
+
}
|
|
6236
|
+
push(item) {
|
|
6237
|
+
this.items.push(item);
|
|
6238
|
+
this.onChange?.();
|
|
6239
|
+
}
|
|
6240
|
+
shift() {
|
|
6241
|
+
const item = this.items.shift();
|
|
6242
|
+
if (item) {
|
|
6243
|
+
this.onChange?.();
|
|
6244
|
+
}
|
|
6245
|
+
return item;
|
|
6246
|
+
}
|
|
6247
|
+
/** Remove and return all queued items. */
|
|
6248
|
+
drain() {
|
|
6249
|
+
if (this.items.length === 0) {
|
|
6250
|
+
return [];
|
|
6251
|
+
}
|
|
6252
|
+
const all = this.items.splice(0);
|
|
6253
|
+
this.onChange?.();
|
|
6254
|
+
return all;
|
|
6255
|
+
}
|
|
6256
|
+
/** Copy of current queue contents (for surfacing on events). */
|
|
6257
|
+
snapshot() {
|
|
6258
|
+
return [...this.items];
|
|
6259
|
+
}
|
|
6260
|
+
/** Return the next item without removing it. */
|
|
6261
|
+
peek() {
|
|
6262
|
+
return this.items[0];
|
|
6263
|
+
}
|
|
6264
|
+
get length() {
|
|
6265
|
+
return this.items.length;
|
|
6266
|
+
}
|
|
6267
|
+
};
|
|
6268
|
+
|
|
6008
6269
|
// src/automatedActions/resolve.ts
|
|
6009
6270
|
var NON_ACTION_SENTINELS = /* @__PURE__ */ new Set(["background_results"]);
|
|
6010
6271
|
function resolveAction(text) {
|
|
6011
|
-
const
|
|
6012
|
-
if (!
|
|
6272
|
+
const parsed = parseSentinel(text);
|
|
6273
|
+
if (!parsed) {
|
|
6013
6274
|
return null;
|
|
6014
6275
|
}
|
|
6015
|
-
const triggerName =
|
|
6276
|
+
const { name: triggerName, remainder } = parsed;
|
|
6016
6277
|
if (NON_ACTION_SENTINELS.has(triggerName)) {
|
|
6017
6278
|
return null;
|
|
6018
6279
|
}
|
|
6019
6280
|
let params = {};
|
|
6020
|
-
const remainder = match[2];
|
|
6021
6281
|
if (remainder) {
|
|
6022
6282
|
try {
|
|
6023
6283
|
params = JSON.parse(remainder.split("\n")[0]);
|
|
@@ -6039,114 +6299,155 @@ function resolveAction(text) {
|
|
|
6039
6299
|
body = body.replaceAll(`{{${key}}}`, str);
|
|
6040
6300
|
}
|
|
6041
6301
|
return {
|
|
6042
|
-
message:
|
|
6043
|
-
${body}`,
|
|
6302
|
+
message: automatedMessage(triggerName, body),
|
|
6044
6303
|
next
|
|
6045
6304
|
};
|
|
6046
6305
|
}
|
|
6047
6306
|
|
|
6048
|
-
// src/headless.ts
|
|
6049
|
-
var
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
|
|
6307
|
+
// src/headless/index.ts
|
|
6308
|
+
var log12 = createLogger("headless");
|
|
6309
|
+
var EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
|
|
6310
|
+
var USER_FACING_TOOLS = /* @__PURE__ */ new Set([
|
|
6311
|
+
"promptUser",
|
|
6312
|
+
"confirmDestructiveAction",
|
|
6313
|
+
"presentPublishPlan"
|
|
6314
|
+
]);
|
|
6315
|
+
var HeadlessSession = class {
|
|
6316
|
+
// Configuration
|
|
6317
|
+
opts;
|
|
6318
|
+
config;
|
|
6319
|
+
// Conversation state
|
|
6320
|
+
state = createAgentState();
|
|
6321
|
+
sessionStats = createSessionStats();
|
|
6322
|
+
// Turn lifecycle
|
|
6323
|
+
running = false;
|
|
6324
|
+
currentAbort = null;
|
|
6325
|
+
/** RequestId of the in-flight message command — injected into streamed events. */
|
|
6326
|
+
currentRequestId;
|
|
6327
|
+
/** Guard: track whether terminal `completed` was already sent so we emit exactly one. */
|
|
6328
|
+
completedEmitted = false;
|
|
6329
|
+
turnStart = 0;
|
|
6330
|
+
/**
|
|
6331
|
+
* Onboarding state of the currently-running turn. Captured at runSingleTurn
|
|
6332
|
+
* start so onBackgroundComplete can enqueue background results with the
|
|
6333
|
+
* right state (the triggering turn's state, not a stale one).
|
|
6334
|
+
*/
|
|
6335
|
+
currentOnboardingState;
|
|
6336
|
+
/**
|
|
6337
|
+
* Unified message queue. Holds pending work to deliver after the current
|
|
6338
|
+
* turn completes: chained automated actions, background sub-agent results,
|
|
6339
|
+
* and user messages sent while a turn is running. Strict FIFO. Persisted
|
|
6340
|
+
* to .remy-stats.json so queued work survives process restarts.
|
|
6341
|
+
*/
|
|
6342
|
+
queue;
|
|
6343
|
+
// External tool bridge
|
|
6344
|
+
pendingTools = /* @__PURE__ */ new Map();
|
|
6345
|
+
earlyResults = /* @__PURE__ */ new Map();
|
|
6346
|
+
// Tool block updates from background completions (separate from the message queue)
|
|
6347
|
+
pendingBlockUpdates = [];
|
|
6348
|
+
// Tool lifecycle management — shared across all nesting depths
|
|
6349
|
+
toolRegistry = new ToolRegistry();
|
|
6350
|
+
// IO
|
|
6351
|
+
readline = null;
|
|
6352
|
+
constructor(opts = {}) {
|
|
6353
|
+
this.opts = opts;
|
|
6354
|
+
}
|
|
6355
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
6356
|
+
// Lifecycle
|
|
6357
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
6358
|
+
async start() {
|
|
6359
|
+
const stderrWrite = (...args) => {
|
|
6360
|
+
process.stderr.write(args.map(String).join(" ") + "\n");
|
|
6361
|
+
};
|
|
6362
|
+
console.log = stderrWrite;
|
|
6363
|
+
console.warn = stderrWrite;
|
|
6364
|
+
console.info = stderrWrite;
|
|
6365
|
+
if (this.opts.lspUrl) {
|
|
6366
|
+
setLspBaseUrl(this.opts.lspUrl);
|
|
6367
|
+
}
|
|
6368
|
+
this.config = resolveConfig({
|
|
6369
|
+
apiKey: this.opts.apiKey,
|
|
6370
|
+
baseUrl: this.opts.baseUrl
|
|
6371
|
+
});
|
|
6372
|
+
const resumed = loadSession(this.state);
|
|
6373
|
+
this.queue = new MessageQueue(loadQueue(), () => this.persistStats());
|
|
6374
|
+
if (resumed) {
|
|
6375
|
+
this.emit("session_restored", {
|
|
6376
|
+
messageCount: this.state.messages.length,
|
|
6377
|
+
...this.queueFields()
|
|
6378
|
+
});
|
|
6077
6379
|
}
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6380
|
+
this.toolRegistry.onEvent = this.onEvent;
|
|
6381
|
+
this.readline = createInterface({ input: process.stdin });
|
|
6382
|
+
this.readline.on("line", this.handleStdinLine);
|
|
6383
|
+
this.readline.on("close", () => {
|
|
6384
|
+
this.emit("stopping");
|
|
6385
|
+
this.emit("stopped");
|
|
6386
|
+
process.exit(0);
|
|
6387
|
+
});
|
|
6388
|
+
process.on("SIGTERM", this.shutdown);
|
|
6389
|
+
process.on("SIGINT", this.shutdown);
|
|
6390
|
+
this.emit("ready", this.queueFields());
|
|
6081
6391
|
}
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
process.
|
|
6086
|
-
};
|
|
6087
|
-
console.log = stderrWrite;
|
|
6088
|
-
console.warn = stderrWrite;
|
|
6089
|
-
console.info = stderrWrite;
|
|
6090
|
-
if (opts.lspUrl) {
|
|
6091
|
-
setLspBaseUrl(opts.lspUrl);
|
|
6092
|
-
}
|
|
6093
|
-
const config = resolveConfig({
|
|
6094
|
-
apiKey: opts.apiKey,
|
|
6095
|
-
baseUrl: opts.baseUrl
|
|
6096
|
-
});
|
|
6097
|
-
const state = createAgentState();
|
|
6098
|
-
const resumed = loadSession(state);
|
|
6099
|
-
if (resumed) {
|
|
6100
|
-
emit("session_restored", { messageCount: state.messages.length });
|
|
6101
|
-
}
|
|
6102
|
-
let running = false;
|
|
6103
|
-
let currentAbort = null;
|
|
6104
|
-
let currentRequestId;
|
|
6105
|
-
let completedEmitted = false;
|
|
6106
|
-
let turnStart = 0;
|
|
6107
|
-
let pendingNextAction;
|
|
6108
|
-
const EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
|
|
6109
|
-
const pendingTools = /* @__PURE__ */ new Map();
|
|
6110
|
-
const earlyResults = /* @__PURE__ */ new Map();
|
|
6111
|
-
const toolRegistry = new ToolRegistry();
|
|
6112
|
-
const sessionStats = {
|
|
6113
|
-
messageCount: 0,
|
|
6114
|
-
turns: 0,
|
|
6115
|
-
totalInputTokens: 0,
|
|
6116
|
-
totalOutputTokens: 0,
|
|
6117
|
-
totalCacheCreationTokens: 0,
|
|
6118
|
-
totalCacheReadTokens: 0,
|
|
6119
|
-
lastContextSize: 0,
|
|
6120
|
-
compactionInProgress: false,
|
|
6121
|
-
updatedAt: 0
|
|
6392
|
+
shutdown = () => {
|
|
6393
|
+
this.emit("stopping");
|
|
6394
|
+
this.emit("stopped");
|
|
6395
|
+
process.exit(0);
|
|
6122
6396
|
};
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6397
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
6398
|
+
// Wire protocol
|
|
6399
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
6400
|
+
emit(event, data, requestId) {
|
|
6401
|
+
const payload = { event, ...data };
|
|
6402
|
+
if (requestId) {
|
|
6403
|
+
payload.requestId = requestId;
|
|
6127
6404
|
}
|
|
6128
|
-
|
|
6129
|
-
const xmlParts = results.map(
|
|
6130
|
-
(r) => `<tool_result id="${r.toolCallId}" name="${r.name}">
|
|
6131
|
-
${r.result}
|
|
6132
|
-
</tool_result>`
|
|
6133
|
-
).join("\n\n");
|
|
6134
|
-
const message = `@@automated::background_results@@
|
|
6135
|
-
This is an automated message containing the result of a tool call that has been working in the background. This is not a direct message from the user.
|
|
6136
|
-
<background_results>
|
|
6137
|
-
${xmlParts}
|
|
6138
|
-
</background_results>`;
|
|
6139
|
-
const bgRequestId = `bg-${Date.now()}`;
|
|
6140
|
-
handleMessage({ action: "message", text: message }, bgRequestId);
|
|
6405
|
+
process.stdout.write(JSON.stringify(payload) + "\n");
|
|
6141
6406
|
}
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6407
|
+
/**
|
|
6408
|
+
* Emit a `completed` event and mark completedEmitted. Includes
|
|
6409
|
+
* `queuedMessages` if the queue has items (sandbox uses this to know the
|
|
6410
|
+
* agent is still busy with pipeline work).
|
|
6411
|
+
*/
|
|
6412
|
+
emitCompleted(rid, data) {
|
|
6413
|
+
this.emit("completed", { ...data, ...this.queueFields() }, rid);
|
|
6414
|
+
this.completedEmitted = true;
|
|
6415
|
+
}
|
|
6416
|
+
/** Returns `{ queuedMessages }` when the queue is non-empty, else empty object. */
|
|
6417
|
+
queueFields() {
|
|
6418
|
+
return this.queue.length > 0 ? { queuedMessages: this.queue.snapshot() } : {};
|
|
6419
|
+
}
|
|
6420
|
+
/** Dispatch a simple (non-streaming) command: call handler, emit response + completed. */
|
|
6421
|
+
dispatchSimple(requestId, eventName, handler) {
|
|
6422
|
+
try {
|
|
6423
|
+
const data = handler();
|
|
6424
|
+
if (eventName) {
|
|
6425
|
+
this.emit(eventName, data, requestId);
|
|
6426
|
+
}
|
|
6427
|
+
this.emit("completed", { success: true }, requestId);
|
|
6428
|
+
} catch (err) {
|
|
6429
|
+
this.emit("completed", { success: false, error: err.message }, requestId);
|
|
6430
|
+
}
|
|
6431
|
+
}
|
|
6432
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
6433
|
+
// Stats + queue persistence
|
|
6434
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
6435
|
+
/** Persist sessionStats + queue snapshot to .remy-stats.json. */
|
|
6436
|
+
persistStats() {
|
|
6437
|
+
this.sessionStats.updatedAt = Date.now();
|
|
6438
|
+
writeStats(this.sessionStats, this.queue.snapshot());
|
|
6439
|
+
}
|
|
6440
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
6441
|
+
// Background completions (tool-block mutation; message delivery via queue)
|
|
6442
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
6443
|
+
/** Apply queued tool block updates to state.messages. Safe to call any time. */
|
|
6444
|
+
applyPendingBlockUpdates() {
|
|
6445
|
+
if (this.pendingBlockUpdates.length === 0) {
|
|
6145
6446
|
return;
|
|
6146
6447
|
}
|
|
6147
|
-
const updates = pendingBlockUpdates.splice(0);
|
|
6448
|
+
const updates = this.pendingBlockUpdates.splice(0);
|
|
6148
6449
|
for (const update of updates) {
|
|
6149
|
-
for (const msg of state.messages) {
|
|
6450
|
+
for (const msg of this.state.messages) {
|
|
6150
6451
|
if (!Array.isArray(msg.content)) {
|
|
6151
6452
|
continue;
|
|
6152
6453
|
}
|
|
@@ -6161,60 +6462,65 @@ ${xmlParts}
|
|
|
6161
6462
|
}
|
|
6162
6463
|
}
|
|
6163
6464
|
}
|
|
6465
|
+
saveSession(this.state);
|
|
6164
6466
|
}
|
|
6165
|
-
|
|
6467
|
+
/** Drain pending compaction summaries and insert at a safe point. */
|
|
6468
|
+
applyPendingSummaries() {
|
|
6166
6469
|
const summaries = getPendingSummaries();
|
|
6167
6470
|
if (summaries.length === 0) {
|
|
6168
6471
|
return;
|
|
6169
6472
|
}
|
|
6170
|
-
const idx = findSafeInsertionPoint(state.messages);
|
|
6171
|
-
state.messages.splice(idx, 0, ...summaries);
|
|
6172
|
-
saveSession(state);
|
|
6473
|
+
const idx = findSafeInsertionPoint(this.state.messages);
|
|
6474
|
+
this.state.messages.splice(idx, 0, ...summaries);
|
|
6475
|
+
saveSession(this.state);
|
|
6173
6476
|
}
|
|
6174
|
-
|
|
6175
|
-
pendingBlockUpdates.push({ toolCallId, result, subAgentMessages });
|
|
6176
|
-
|
|
6477
|
+
onBackgroundComplete = (toolCallId, name, result, subAgentMessages) => {
|
|
6478
|
+
this.pendingBlockUpdates.push({ toolCallId, result, subAgentMessages });
|
|
6479
|
+
log12.info("Background complete", {
|
|
6177
6480
|
toolCallId,
|
|
6178
6481
|
name,
|
|
6179
|
-
requestId: currentRequestId
|
|
6482
|
+
requestId: this.currentRequestId
|
|
6180
6483
|
});
|
|
6181
|
-
onEvent({
|
|
6484
|
+
this.onEvent({
|
|
6182
6485
|
type: "tool_background_complete",
|
|
6183
6486
|
id: toolCallId,
|
|
6184
6487
|
name,
|
|
6185
6488
|
result
|
|
6186
6489
|
});
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6490
|
+
this.queue.push({
|
|
6491
|
+
command: {
|
|
6492
|
+
action: "message",
|
|
6493
|
+
text: buildBackgroundResultsMessage([{ toolCallId, name, result }]),
|
|
6494
|
+
...this.currentOnboardingState && {
|
|
6495
|
+
onboardingState: this.currentOnboardingState
|
|
6496
|
+
}
|
|
6497
|
+
},
|
|
6498
|
+
source: "background",
|
|
6499
|
+
enqueuedAt: Date.now()
|
|
6192
6500
|
});
|
|
6193
|
-
if (!running) {
|
|
6194
|
-
applyPendingBlockUpdates();
|
|
6195
|
-
|
|
6501
|
+
if (!this.running) {
|
|
6502
|
+
this.applyPendingBlockUpdates();
|
|
6503
|
+
this.kickDrain();
|
|
6196
6504
|
}
|
|
6197
|
-
}
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
function resolveExternalTool(id, name, _input) {
|
|
6204
|
-
const early = earlyResults.get(id);
|
|
6505
|
+
};
|
|
6506
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
6507
|
+
// External tool bridge
|
|
6508
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
6509
|
+
resolveExternalTool = (id, name, _input) => {
|
|
6510
|
+
const early = this.earlyResults.get(id);
|
|
6205
6511
|
if (early !== void 0) {
|
|
6206
|
-
earlyResults.delete(id);
|
|
6512
|
+
this.earlyResults.delete(id);
|
|
6207
6513
|
return Promise.resolve(early);
|
|
6208
6514
|
}
|
|
6209
6515
|
const shouldTimeout = !USER_FACING_TOOLS.has(name);
|
|
6210
6516
|
return new Promise((resolve2) => {
|
|
6211
6517
|
const timeout = shouldTimeout ? setTimeout(() => {
|
|
6212
|
-
pendingTools.delete(id);
|
|
6518
|
+
this.pendingTools.delete(id);
|
|
6213
6519
|
resolve2(
|
|
6214
6520
|
"Error: Tool timed out \u2014 no response from the app environment after 5 minutes."
|
|
6215
6521
|
);
|
|
6216
6522
|
}, EXTERNAL_TOOL_TIMEOUT_MS) : void 0;
|
|
6217
|
-
pendingTools.set(id, {
|
|
6523
|
+
this.pendingTools.set(id, {
|
|
6218
6524
|
resolve: (result) => {
|
|
6219
6525
|
clearTimeout(timeout);
|
|
6220
6526
|
resolve2(result);
|
|
@@ -6222,57 +6528,51 @@ ${xmlParts}
|
|
|
6222
6528
|
timeout
|
|
6223
6529
|
});
|
|
6224
6530
|
});
|
|
6225
|
-
}
|
|
6226
|
-
|
|
6227
|
-
|
|
6531
|
+
};
|
|
6532
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
6533
|
+
// AgentEvent → wire protocol translation
|
|
6534
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
6535
|
+
onEvent = (e) => {
|
|
6536
|
+
const rid = this.currentRequestId;
|
|
6228
6537
|
switch (e.type) {
|
|
6229
6538
|
case "turn_started":
|
|
6230
|
-
emit("turn_started", {}, rid);
|
|
6539
|
+
this.emit("turn_started", {}, rid);
|
|
6540
|
+
return;
|
|
6541
|
+
case "user_message":
|
|
6542
|
+
this.emit("user_message", { text: e.text }, rid);
|
|
6231
6543
|
return;
|
|
6232
|
-
// Terminal events — translate to `completed
|
|
6544
|
+
// Terminal events — translate to `completed`.
|
|
6545
|
+
// Post-turn queue drain happens in handleMessage AFTER runTurn returns,
|
|
6546
|
+
// so that `running` is held across the drain and no user message can
|
|
6547
|
+
// slip in mid-pipeline.
|
|
6233
6548
|
case "turn_done":
|
|
6234
|
-
completedEmitted = true;
|
|
6235
6549
|
if (e.stats) {
|
|
6236
|
-
sessionStats.turns++;
|
|
6237
|
-
sessionStats.totalInputTokens += e.stats.inputTokens;
|
|
6238
|
-
sessionStats.totalOutputTokens += e.stats.outputTokens;
|
|
6239
|
-
sessionStats.totalCacheCreationTokens += e.stats.cacheCreationTokens ?? 0;
|
|
6240
|
-
sessionStats.totalCacheReadTokens += e.stats.cacheReadTokens ?? 0;
|
|
6241
|
-
sessionStats.lastContextSize = e.stats.lastCallInputTokens ?? e.stats.inputTokens;
|
|
6242
|
-
}
|
|
6243
|
-
sessionStats.messageCount = state.messages.length;
|
|
6244
|
-
sessionStats.updatedAt = Date.now();
|
|
6245
|
-
try {
|
|
6246
|
-
writeFileSync(".remy-stats.json", JSON.stringify(sessionStats));
|
|
6247
|
-
} catch {
|
|
6550
|
+
this.sessionStats.turns++;
|
|
6551
|
+
this.sessionStats.totalInputTokens += e.stats.inputTokens;
|
|
6552
|
+
this.sessionStats.totalOutputTokens += e.stats.outputTokens;
|
|
6553
|
+
this.sessionStats.totalCacheCreationTokens += e.stats.cacheCreationTokens ?? 0;
|
|
6554
|
+
this.sessionStats.totalCacheReadTokens += e.stats.cacheReadTokens ?? 0;
|
|
6555
|
+
this.sessionStats.lastContextSize = e.stats.lastCallInputTokens ?? e.stats.inputTokens;
|
|
6248
6556
|
}
|
|
6249
|
-
|
|
6557
|
+
this.sessionStats.messageCount = this.state.messages.length;
|
|
6558
|
+
this.persistStats();
|
|
6559
|
+
this.emitCompleted(rid, {
|
|
6560
|
+
success: true,
|
|
6561
|
+
durationMs: Date.now() - this.turnStart
|
|
6562
|
+
});
|
|
6563
|
+
return;
|
|
6564
|
+
case "turn_cancelled": {
|
|
6565
|
+
this.emit(
|
|
6250
6566
|
"completed",
|
|
6251
|
-
{ success:
|
|
6567
|
+
{ success: false, error: "cancelled", ...this.queueFields() },
|
|
6252
6568
|
rid
|
|
6253
6569
|
);
|
|
6254
|
-
|
|
6255
|
-
applyPendingSummaries();
|
|
6256
|
-
applyPendingBlockUpdates();
|
|
6257
|
-
flushBackgroundQueue();
|
|
6258
|
-
if (pendingNextAction) {
|
|
6259
|
-
const next = pendingNextAction;
|
|
6260
|
-
pendingNextAction = void 0;
|
|
6261
|
-
handleMessage(
|
|
6262
|
-
{ action: "message", text: `@@automated::${next}@@` },
|
|
6263
|
-
`chain-${Date.now()}`
|
|
6264
|
-
);
|
|
6265
|
-
}
|
|
6266
|
-
}, 0);
|
|
6267
|
-
return;
|
|
6268
|
-
case "turn_cancelled":
|
|
6269
|
-
completedEmitted = true;
|
|
6270
|
-
pendingNextAction = void 0;
|
|
6271
|
-
emit("completed", { success: false, error: "cancelled" }, rid);
|
|
6570
|
+
this.completedEmitted = true;
|
|
6272
6571
|
return;
|
|
6572
|
+
}
|
|
6273
6573
|
// Streaming events — forward with requestId
|
|
6274
6574
|
case "text":
|
|
6275
|
-
emit(
|
|
6575
|
+
this.emit(
|
|
6276
6576
|
"text",
|
|
6277
6577
|
{
|
|
6278
6578
|
text: e.text,
|
|
@@ -6282,7 +6582,7 @@ ${xmlParts}
|
|
|
6282
6582
|
);
|
|
6283
6583
|
return;
|
|
6284
6584
|
case "thinking":
|
|
6285
|
-
emit(
|
|
6585
|
+
this.emit(
|
|
6286
6586
|
"thinking",
|
|
6287
6587
|
{
|
|
6288
6588
|
text: e.text,
|
|
@@ -6292,7 +6592,7 @@ ${xmlParts}
|
|
|
6292
6592
|
);
|
|
6293
6593
|
return;
|
|
6294
6594
|
case "tool_input_delta":
|
|
6295
|
-
emit(
|
|
6595
|
+
this.emit(
|
|
6296
6596
|
"tool_input_delta",
|
|
6297
6597
|
{
|
|
6298
6598
|
id: e.id,
|
|
@@ -6304,7 +6604,7 @@ ${xmlParts}
|
|
|
6304
6604
|
);
|
|
6305
6605
|
return;
|
|
6306
6606
|
case "tool_start":
|
|
6307
|
-
emit(
|
|
6607
|
+
this.emit(
|
|
6308
6608
|
"tool_start",
|
|
6309
6609
|
{
|
|
6310
6610
|
id: e.id,
|
|
@@ -6318,7 +6618,7 @@ ${xmlParts}
|
|
|
6318
6618
|
);
|
|
6319
6619
|
return;
|
|
6320
6620
|
case "tool_done":
|
|
6321
|
-
emit(
|
|
6621
|
+
this.emit(
|
|
6322
6622
|
"tool_done",
|
|
6323
6623
|
{
|
|
6324
6624
|
id: e.id,
|
|
@@ -6331,7 +6631,7 @@ ${xmlParts}
|
|
|
6331
6631
|
);
|
|
6332
6632
|
return;
|
|
6333
6633
|
case "tool_background_complete":
|
|
6334
|
-
emit(
|
|
6634
|
+
this.emit(
|
|
6335
6635
|
"tool_background_complete",
|
|
6336
6636
|
{
|
|
6337
6637
|
id: e.id,
|
|
@@ -6343,7 +6643,7 @@ ${xmlParts}
|
|
|
6343
6643
|
);
|
|
6344
6644
|
return;
|
|
6345
6645
|
case "tool_stopped":
|
|
6346
|
-
emit(
|
|
6646
|
+
this.emit(
|
|
6347
6647
|
"tool_stopped",
|
|
6348
6648
|
{
|
|
6349
6649
|
id: e.id,
|
|
@@ -6355,7 +6655,7 @@ ${xmlParts}
|
|
|
6355
6655
|
);
|
|
6356
6656
|
return;
|
|
6357
6657
|
case "tool_restarted":
|
|
6358
|
-
emit(
|
|
6658
|
+
this.emit(
|
|
6359
6659
|
"tool_restarted",
|
|
6360
6660
|
{
|
|
6361
6661
|
id: e.id,
|
|
@@ -6367,7 +6667,7 @@ ${xmlParts}
|
|
|
6367
6667
|
);
|
|
6368
6668
|
return;
|
|
6369
6669
|
case "status":
|
|
6370
|
-
emit(
|
|
6670
|
+
this.emit(
|
|
6371
6671
|
"status",
|
|
6372
6672
|
{
|
|
6373
6673
|
message: e.message,
|
|
@@ -6377,147 +6677,27 @@ ${xmlParts}
|
|
|
6377
6677
|
);
|
|
6378
6678
|
return;
|
|
6379
6679
|
case "error":
|
|
6380
|
-
emit("error", { error: e.error }, rid);
|
|
6680
|
+
this.emit("error", { error: e.error }, rid);
|
|
6381
6681
|
return;
|
|
6382
6682
|
}
|
|
6383
|
-
}
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
|
|
6393
|
-
|
|
6394
|
-
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
}
|
|
6399
|
-
const ext = extname(name);
|
|
6400
|
-
const base = name.slice(0, name.length - ext.length);
|
|
6401
|
-
let counter = 1;
|
|
6402
|
-
while (existsSync(join(UPLOADS_DIR, `${base}-${counter}${ext}`))) {
|
|
6403
|
-
counter++;
|
|
6404
|
-
}
|
|
6405
|
-
return `${base}-${counter}${ext}`;
|
|
6406
|
-
}
|
|
6407
|
-
const IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
6408
|
-
".png",
|
|
6409
|
-
".jpg",
|
|
6410
|
-
".jpeg",
|
|
6411
|
-
".gif",
|
|
6412
|
-
".webp",
|
|
6413
|
-
".svg",
|
|
6414
|
-
".bmp",
|
|
6415
|
-
".ico",
|
|
6416
|
-
".tiff",
|
|
6417
|
-
".tif",
|
|
6418
|
-
".avif",
|
|
6419
|
-
".heic",
|
|
6420
|
-
".heif"
|
|
6421
|
-
]);
|
|
6422
|
-
function isImageAttachment(att) {
|
|
6423
|
-
const name = att.filename || filenameFromUrl(att.url);
|
|
6424
|
-
return IMAGE_EXTENSIONS.has(extname(name).toLowerCase());
|
|
6425
|
-
}
|
|
6426
|
-
async function persistAttachments(attachments) {
|
|
6427
|
-
const nonVoice = attachments.filter((a) => !a.isVoice);
|
|
6428
|
-
if (nonVoice.length === 0) {
|
|
6429
|
-
return { documents: [], images: [] };
|
|
6430
|
-
}
|
|
6431
|
-
mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
6432
|
-
const results = await Promise.allSettled(
|
|
6433
|
-
nonVoice.map(async (att) => {
|
|
6434
|
-
const name = resolveUniqueFilename(
|
|
6435
|
-
att.filename || filenameFromUrl(att.url)
|
|
6436
|
-
);
|
|
6437
|
-
const localPath = join(UPLOADS_DIR, name);
|
|
6438
|
-
const res = await fetch(att.url, {
|
|
6439
|
-
signal: AbortSignal.timeout(3e4)
|
|
6440
|
-
});
|
|
6441
|
-
if (!res.ok) {
|
|
6442
|
-
throw new Error(`HTTP ${res.status} downloading ${att.url}`);
|
|
6443
|
-
}
|
|
6444
|
-
const buffer = Buffer.from(await res.arrayBuffer());
|
|
6445
|
-
await writeFile(localPath, buffer);
|
|
6446
|
-
log11.info("Attachment saved", {
|
|
6447
|
-
filename: name,
|
|
6448
|
-
path: localPath,
|
|
6449
|
-
bytes: buffer.length
|
|
6450
|
-
});
|
|
6451
|
-
let extractedTextPath;
|
|
6452
|
-
if (att.extractedTextUrl) {
|
|
6453
|
-
try {
|
|
6454
|
-
const textRes = await fetch(att.extractedTextUrl, {
|
|
6455
|
-
signal: AbortSignal.timeout(3e4)
|
|
6456
|
-
});
|
|
6457
|
-
if (textRes.ok) {
|
|
6458
|
-
extractedTextPath = `${localPath}.txt`;
|
|
6459
|
-
await writeFile(extractedTextPath, await textRes.text(), "utf-8");
|
|
6460
|
-
log11.info("Extracted text saved", { path: extractedTextPath });
|
|
6461
|
-
}
|
|
6462
|
-
} catch {
|
|
6463
|
-
}
|
|
6464
|
-
}
|
|
6465
|
-
return { filename: name, localPath, extractedTextPath };
|
|
6466
|
-
})
|
|
6467
|
-
);
|
|
6468
|
-
const settled = results.map((r, i) => ({
|
|
6469
|
-
result: r.status === "fulfilled" ? r.value : null,
|
|
6470
|
-
isImage: isImageAttachment(nonVoice[i])
|
|
6471
|
-
}));
|
|
6472
|
-
return {
|
|
6473
|
-
documents: settled.filter((s) => !s.isImage).map((s) => s.result),
|
|
6474
|
-
images: settled.filter((s) => s.isImage).map((s) => s.result)
|
|
6475
|
-
};
|
|
6476
|
-
}
|
|
6477
|
-
function buildUploadHeader(results) {
|
|
6478
|
-
const succeeded = results.filter(Boolean);
|
|
6479
|
-
if (succeeded.length === 0) {
|
|
6480
|
-
return "";
|
|
6481
|
-
}
|
|
6482
|
-
if (succeeded.length === 1) {
|
|
6483
|
-
const r = succeeded[0];
|
|
6484
|
-
const parts = [`[Uploaded file: ${r.localPath}`];
|
|
6485
|
-
if (r.extractedTextPath) {
|
|
6486
|
-
parts.push(`extracted text: ${r.extractedTextPath}`);
|
|
6487
|
-
}
|
|
6488
|
-
return parts.join(" \u2014 ") + "]";
|
|
6489
|
-
}
|
|
6490
|
-
const lines = succeeded.map((r) => {
|
|
6491
|
-
if (r.extractedTextPath) {
|
|
6492
|
-
return `- ${r.localPath} (extracted text: ${r.extractedTextPath})`;
|
|
6493
|
-
}
|
|
6494
|
-
return `- ${r.localPath}`;
|
|
6495
|
-
});
|
|
6496
|
-
return `[Uploaded files]
|
|
6497
|
-
${lines.join("\n")}`;
|
|
6498
|
-
}
|
|
6499
|
-
async function handleMessage(parsed, requestId) {
|
|
6500
|
-
if (running) {
|
|
6501
|
-
emit(
|
|
6502
|
-
"error",
|
|
6503
|
-
{ error: "Agent is already processing a message" },
|
|
6504
|
-
requestId
|
|
6505
|
-
);
|
|
6506
|
-
emit(
|
|
6507
|
-
"completed",
|
|
6508
|
-
{ success: false, error: "Agent is already processing a message" },
|
|
6509
|
-
requestId
|
|
6510
|
-
);
|
|
6511
|
-
return;
|
|
6512
|
-
}
|
|
6513
|
-
running = true;
|
|
6514
|
-
currentRequestId = requestId;
|
|
6515
|
-
currentAbort = new AbortController();
|
|
6516
|
-
completedEmitted = false;
|
|
6517
|
-
turnStart = Date.now();
|
|
6683
|
+
};
|
|
6684
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
6685
|
+
// Message command handler (long-running / streaming)
|
|
6686
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
6687
|
+
/**
|
|
6688
|
+
* Run one turn (without acquiring the `running` lock). Called by
|
|
6689
|
+
* handleMessage for the initial turn, then repeatedly for each queued
|
|
6690
|
+
* message — `running` stays held across the queue drain so no user
|
|
6691
|
+
* message can slip in mid-pipeline.
|
|
6692
|
+
*/
|
|
6693
|
+
async runSingleTurn(parsed, requestId) {
|
|
6694
|
+
this.currentRequestId = requestId;
|
|
6695
|
+
this.currentAbort = new AbortController();
|
|
6696
|
+
this.completedEmitted = false;
|
|
6697
|
+
this.turnStart = Date.now();
|
|
6518
6698
|
const attachments = parsed.attachments;
|
|
6519
6699
|
if (attachments?.length) {
|
|
6520
|
-
|
|
6700
|
+
log12.info("Message has attachments", {
|
|
6521
6701
|
count: attachments.length,
|
|
6522
6702
|
urls: attachments.map((a) => a.url)
|
|
6523
6703
|
});
|
|
@@ -6534,143 +6714,266 @@ ${lines.join("\n")}`;
|
|
|
6534
6714
|
${userMessage}` : header;
|
|
6535
6715
|
}
|
|
6536
6716
|
} catch (err) {
|
|
6537
|
-
|
|
6717
|
+
log12.warn("Attachment persistence failed", { error: err.message });
|
|
6538
6718
|
}
|
|
6539
6719
|
}
|
|
6540
6720
|
let resolved = null;
|
|
6541
6721
|
try {
|
|
6542
6722
|
resolved = resolveAction(userMessage);
|
|
6543
6723
|
} catch (err) {
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
|
|
6547
|
-
|
|
6548
|
-
);
|
|
6724
|
+
this.emitCompleted(requestId, {
|
|
6725
|
+
success: false,
|
|
6726
|
+
error: err.message || "Failed to resolve action"
|
|
6727
|
+
});
|
|
6549
6728
|
return;
|
|
6550
6729
|
}
|
|
6551
|
-
pendingNextAction = void 0;
|
|
6552
6730
|
if (resolved !== null) {
|
|
6553
6731
|
userMessage = resolved.message;
|
|
6554
|
-
pendingNextAction = resolved.next;
|
|
6555
6732
|
}
|
|
6556
6733
|
const isHidden = resolved !== null || !!parsed.hidden;
|
|
6557
|
-
|
|
6558
|
-
if (rawText.startsWith("@@automated::approvePlan@@")) {
|
|
6559
|
-
try {
|
|
6560
|
-
const plan = readFileSync(".remy-plan.md", "utf-8");
|
|
6561
|
-
writeFileSync(
|
|
6562
|
-
".remy-plan.md",
|
|
6563
|
-
plan.replace(/^status:\s*pending/m, "status: approved"),
|
|
6564
|
-
"utf-8"
|
|
6565
|
-
);
|
|
6566
|
-
} catch {
|
|
6567
|
-
}
|
|
6568
|
-
} else if (rawText.startsWith("@@automated::rejectPlan@@")) {
|
|
6569
|
-
try {
|
|
6570
|
-
unlinkSync(".remy-plan.md");
|
|
6571
|
-
} catch {
|
|
6572
|
-
}
|
|
6573
|
-
}
|
|
6734
|
+
applyPlanFileSideEffect(parsed.text ?? "");
|
|
6574
6735
|
const onboardingState = parsed.onboardingState ?? "onboardingFinished";
|
|
6736
|
+
this.currentOnboardingState = onboardingState;
|
|
6575
6737
|
const system = buildSystemPrompt(
|
|
6576
6738
|
onboardingState,
|
|
6577
6739
|
parsed.viewContext
|
|
6578
6740
|
);
|
|
6741
|
+
if (resolved?.next) {
|
|
6742
|
+
this.queue.push({
|
|
6743
|
+
command: {
|
|
6744
|
+
action: "message",
|
|
6745
|
+
text: sentinel(resolved.next),
|
|
6746
|
+
onboardingState
|
|
6747
|
+
},
|
|
6748
|
+
source: "chain",
|
|
6749
|
+
enqueuedAt: Date.now()
|
|
6750
|
+
});
|
|
6751
|
+
}
|
|
6579
6752
|
try {
|
|
6580
6753
|
await runTurn({
|
|
6581
|
-
state,
|
|
6754
|
+
state: this.state,
|
|
6582
6755
|
userMessage,
|
|
6583
6756
|
attachments,
|
|
6584
|
-
apiConfig: config,
|
|
6757
|
+
apiConfig: this.config,
|
|
6585
6758
|
system,
|
|
6586
|
-
model: opts.model,
|
|
6759
|
+
model: this.opts.model,
|
|
6587
6760
|
onboardingState,
|
|
6588
6761
|
requestId,
|
|
6589
|
-
signal: currentAbort.signal,
|
|
6590
|
-
onEvent,
|
|
6591
|
-
resolveExternalTool,
|
|
6762
|
+
signal: this.currentAbort.signal,
|
|
6763
|
+
onEvent: this.onEvent,
|
|
6764
|
+
resolveExternalTool: this.resolveExternalTool,
|
|
6592
6765
|
hidden: isHidden,
|
|
6593
|
-
toolRegistry,
|
|
6594
|
-
onBackgroundComplete
|
|
6766
|
+
toolRegistry: this.toolRegistry,
|
|
6767
|
+
onBackgroundComplete: this.onBackgroundComplete
|
|
6595
6768
|
});
|
|
6596
|
-
if (!completedEmitted) {
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
);
|
|
6769
|
+
if (!this.completedEmitted) {
|
|
6770
|
+
this.emitCompleted(requestId, {
|
|
6771
|
+
success: false,
|
|
6772
|
+
error: "Turn ended unexpectedly"
|
|
6773
|
+
});
|
|
6602
6774
|
}
|
|
6603
|
-
|
|
6775
|
+
log12.info("Turn complete", {
|
|
6604
6776
|
requestId,
|
|
6605
|
-
durationMs: Date.now() - turnStart
|
|
6777
|
+
durationMs: Date.now() - this.turnStart
|
|
6606
6778
|
});
|
|
6607
6779
|
} catch (err) {
|
|
6608
|
-
if (!completedEmitted) {
|
|
6609
|
-
emit("error", { error: err.message }, requestId);
|
|
6610
|
-
|
|
6780
|
+
if (!this.completedEmitted) {
|
|
6781
|
+
this.emit("error", { error: err.message }, requestId);
|
|
6782
|
+
this.emitCompleted(requestId, {
|
|
6783
|
+
success: false,
|
|
6784
|
+
error: err.message
|
|
6785
|
+
});
|
|
6611
6786
|
}
|
|
6612
|
-
|
|
6787
|
+
log12.warn("Command failed", {
|
|
6613
6788
|
action: "message",
|
|
6614
6789
|
requestId,
|
|
6615
6790
|
error: err.message
|
|
6616
6791
|
});
|
|
6792
|
+
this.queue.drain();
|
|
6793
|
+
}
|
|
6794
|
+
this.applyPendingSummaries();
|
|
6795
|
+
this.applyPendingBlockUpdates();
|
|
6796
|
+
}
|
|
6797
|
+
async handleMessage(parsed, requestId) {
|
|
6798
|
+
if (this.running) {
|
|
6799
|
+
const command = { ...parsed };
|
|
6800
|
+
if (requestId && command.requestId === void 0) {
|
|
6801
|
+
command.requestId = requestId;
|
|
6802
|
+
}
|
|
6803
|
+
this.queue.push({
|
|
6804
|
+
command,
|
|
6805
|
+
source: "user",
|
|
6806
|
+
enqueuedAt: Date.now()
|
|
6807
|
+
});
|
|
6808
|
+
this.emit(
|
|
6809
|
+
"queued",
|
|
6810
|
+
{ position: this.queue.length, ...this.queueFields() },
|
|
6811
|
+
requestId
|
|
6812
|
+
);
|
|
6813
|
+
return;
|
|
6814
|
+
}
|
|
6815
|
+
this.running = true;
|
|
6816
|
+
try {
|
|
6817
|
+
await this.runSingleTurn(parsed, requestId);
|
|
6818
|
+
await this.drainQueueLoop();
|
|
6819
|
+
} finally {
|
|
6820
|
+
this.currentAbort = null;
|
|
6821
|
+
this.currentRequestId = void 0;
|
|
6822
|
+
this.running = false;
|
|
6823
|
+
}
|
|
6824
|
+
}
|
|
6825
|
+
/**
|
|
6826
|
+
* Drain the queue in strict FIFO order. Caller must hold `running = true`.
|
|
6827
|
+
* User messages arriving during the drain will be enqueued behind current items.
|
|
6828
|
+
*
|
|
6829
|
+
* Consecutive background-source items are coalesced into a single turn so
|
|
6830
|
+
* the LLM sees all the background results together and produces one
|
|
6831
|
+
* acknowledgment, not N separate ones.
|
|
6832
|
+
*/
|
|
6833
|
+
async drainQueueLoop() {
|
|
6834
|
+
while (true) {
|
|
6835
|
+
const next = this.queue.shift();
|
|
6836
|
+
if (!next) {
|
|
6837
|
+
break;
|
|
6838
|
+
}
|
|
6839
|
+
if (next.source === "background") {
|
|
6840
|
+
const batch = [next];
|
|
6841
|
+
while (this.queue.peek()?.source === "background") {
|
|
6842
|
+
const more = this.queue.shift();
|
|
6843
|
+
if (more) {
|
|
6844
|
+
batch.push(more);
|
|
6845
|
+
}
|
|
6846
|
+
}
|
|
6847
|
+
const combinedCommand = {
|
|
6848
|
+
action: "message",
|
|
6849
|
+
text: mergeBackgroundResultsMessages(
|
|
6850
|
+
batch.map((b) => b.command.text ?? "")
|
|
6851
|
+
),
|
|
6852
|
+
...this.currentOnboardingState && {
|
|
6853
|
+
onboardingState: this.currentOnboardingState
|
|
6854
|
+
}
|
|
6855
|
+
};
|
|
6856
|
+
await this.runSingleTurn(combinedCommand, `background-${Date.now()}`);
|
|
6857
|
+
continue;
|
|
6858
|
+
}
|
|
6859
|
+
const nextRid = next.command.requestId ?? `${next.source}-${Date.now()}`;
|
|
6860
|
+
await this.runSingleTurn(next.command, nextRid);
|
|
6861
|
+
}
|
|
6862
|
+
}
|
|
6863
|
+
/**
|
|
6864
|
+
* Resume draining the queue when the agent is idle. Acquires the lock,
|
|
6865
|
+
* drains, releases. Used by the `resume` stdin action (sandbox-initiated)
|
|
6866
|
+
* and by kickDrain (background-completion-initiated).
|
|
6867
|
+
*/
|
|
6868
|
+
async resumeQueue() {
|
|
6869
|
+
if (this.running || this.queue.length === 0) {
|
|
6870
|
+
return;
|
|
6871
|
+
}
|
|
6872
|
+
this.running = true;
|
|
6873
|
+
try {
|
|
6874
|
+
await this.drainQueueLoop();
|
|
6875
|
+
} finally {
|
|
6876
|
+
this.currentAbort = null;
|
|
6877
|
+
this.currentRequestId = void 0;
|
|
6878
|
+
this.running = false;
|
|
6617
6879
|
}
|
|
6618
|
-
currentAbort = null;
|
|
6619
|
-
currentRequestId = void 0;
|
|
6620
|
-
running = false;
|
|
6621
6880
|
}
|
|
6622
|
-
|
|
6623
|
-
|
|
6881
|
+
/**
|
|
6882
|
+
* Kick off drainage of the queue when the agent is idle. Used by
|
|
6883
|
+
* onBackgroundComplete (when !running) to deliver results without
|
|
6884
|
+
* racing any currently-synchronous path.
|
|
6885
|
+
*/
|
|
6886
|
+
kickDrain() {
|
|
6887
|
+
if (this.running || this.queue.length === 0) {
|
|
6888
|
+
return;
|
|
6889
|
+
}
|
|
6890
|
+
setTimeout(() => this.resumeQueue(), 0);
|
|
6891
|
+
}
|
|
6892
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
6893
|
+
// Simple command handlers
|
|
6894
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
6895
|
+
handleClear() {
|
|
6896
|
+
clearSession(this.state);
|
|
6897
|
+
return {};
|
|
6898
|
+
}
|
|
6899
|
+
/** Cancel the running turn and drain the queue. Returns the drained items. */
|
|
6900
|
+
handleCancel() {
|
|
6901
|
+
if (this.currentAbort) {
|
|
6902
|
+
this.currentAbort.abort();
|
|
6903
|
+
}
|
|
6904
|
+
for (const [id, pending] of this.pendingTools) {
|
|
6905
|
+
clearTimeout(pending.timeout);
|
|
6906
|
+
pending.resolve("Error: cancelled");
|
|
6907
|
+
this.pendingTools.delete(id);
|
|
6908
|
+
}
|
|
6909
|
+
return this.queue.drain();
|
|
6910
|
+
}
|
|
6911
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
6912
|
+
// Stdin router
|
|
6913
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
6914
|
+
handleStdinLine = async (line) => {
|
|
6624
6915
|
let parsed;
|
|
6625
6916
|
try {
|
|
6626
6917
|
parsed = JSON.parse(line);
|
|
6627
6918
|
} catch {
|
|
6628
|
-
emit("error", { error: "Invalid JSON on stdin" });
|
|
6919
|
+
this.emit("error", { error: "Invalid JSON on stdin" });
|
|
6629
6920
|
return;
|
|
6630
6921
|
}
|
|
6631
6922
|
const { action, requestId } = parsed;
|
|
6632
|
-
|
|
6923
|
+
log12.info("Command received", { action, requestId });
|
|
6633
6924
|
if (action === "tool_result" && parsed.id) {
|
|
6634
6925
|
const id = parsed.id;
|
|
6635
6926
|
const result = parsed.result ?? "";
|
|
6636
|
-
const pending = pendingTools.get(id);
|
|
6927
|
+
const pending = this.pendingTools.get(id);
|
|
6637
6928
|
if (pending) {
|
|
6638
|
-
pendingTools.delete(id);
|
|
6929
|
+
this.pendingTools.delete(id);
|
|
6639
6930
|
pending.resolve(result);
|
|
6640
|
-
} else if (!running) {
|
|
6641
|
-
|
|
6642
|
-
emit("completed", { success: true }, requestId);
|
|
6931
|
+
} else if (!this.running) {
|
|
6932
|
+
log12.info("Late tool_result while idle, dismissing", { id });
|
|
6933
|
+
this.emit("completed", { success: true }, requestId);
|
|
6643
6934
|
} else {
|
|
6644
|
-
earlyResults.set(id, result);
|
|
6935
|
+
this.earlyResults.set(id, result);
|
|
6645
6936
|
}
|
|
6646
6937
|
return;
|
|
6647
6938
|
}
|
|
6648
6939
|
if (action === "get_history") {
|
|
6649
|
-
applyPendingBlockUpdates();
|
|
6650
|
-
dispatchSimple(requestId, "history", () => ({
|
|
6651
|
-
messages: state.messages,
|
|
6652
|
-
running,
|
|
6653
|
-
...running && currentRequestId ? { currentRequestId } : {}
|
|
6940
|
+
this.applyPendingBlockUpdates();
|
|
6941
|
+
this.dispatchSimple(requestId, "history", () => ({
|
|
6942
|
+
messages: this.state.messages,
|
|
6943
|
+
running: this.running,
|
|
6944
|
+
...this.running && this.currentRequestId ? { currentRequestId: this.currentRequestId } : {},
|
|
6945
|
+
...this.queueFields()
|
|
6654
6946
|
}));
|
|
6655
6947
|
return;
|
|
6656
6948
|
}
|
|
6657
6949
|
if (action === "clear") {
|
|
6658
|
-
dispatchSimple(
|
|
6950
|
+
this.dispatchSimple(
|
|
6951
|
+
requestId,
|
|
6952
|
+
"session_cleared",
|
|
6953
|
+
() => this.handleClear()
|
|
6954
|
+
);
|
|
6659
6955
|
return;
|
|
6660
6956
|
}
|
|
6661
6957
|
if (action === "cancel") {
|
|
6662
|
-
handleCancel(
|
|
6663
|
-
emit(
|
|
6958
|
+
const cancelled = this.handleCancel();
|
|
6959
|
+
this.emit(
|
|
6960
|
+
"completed",
|
|
6961
|
+
{
|
|
6962
|
+
success: true,
|
|
6963
|
+
...cancelled.length > 0 && { cancelledMessages: cancelled }
|
|
6964
|
+
},
|
|
6965
|
+
requestId
|
|
6966
|
+
);
|
|
6664
6967
|
return;
|
|
6665
6968
|
}
|
|
6666
6969
|
if (action === "stop_tool") {
|
|
6667
6970
|
const id = parsed.id;
|
|
6668
6971
|
const mode = parsed.mode ?? "hard";
|
|
6669
|
-
const found = toolRegistry.stop(id, mode);
|
|
6972
|
+
const found = this.toolRegistry.stop(id, mode);
|
|
6670
6973
|
if (found) {
|
|
6671
|
-
emit("completed", { success: true }, requestId);
|
|
6974
|
+
this.emit("completed", { success: true }, requestId);
|
|
6672
6975
|
} else {
|
|
6673
|
-
emit(
|
|
6976
|
+
this.emit(
|
|
6674
6977
|
"completed",
|
|
6675
6978
|
{ success: false, error: "Tool not found" },
|
|
6676
6979
|
requestId
|
|
@@ -6681,11 +6984,11 @@ ${userMessage}` : header;
|
|
|
6681
6984
|
if (action === "restart_tool") {
|
|
6682
6985
|
const id = parsed.id;
|
|
6683
6986
|
const patchedInput = parsed.input;
|
|
6684
|
-
const found = toolRegistry.restart(id, patchedInput);
|
|
6987
|
+
const found = this.toolRegistry.restart(id, patchedInput);
|
|
6685
6988
|
if (found) {
|
|
6686
|
-
emit("completed", { success: true }, requestId);
|
|
6989
|
+
this.emit("completed", { success: true }, requestId);
|
|
6687
6990
|
} else {
|
|
6688
|
-
emit(
|
|
6991
|
+
this.emit(
|
|
6689
6992
|
"completed",
|
|
6690
6993
|
{ success: false, error: "Tool not found" },
|
|
6691
6994
|
requestId
|
|
@@ -6694,64 +6997,60 @@ ${userMessage}` : header;
|
|
|
6694
6997
|
return;
|
|
6695
6998
|
}
|
|
6696
6999
|
if (action === "compact") {
|
|
6697
|
-
triggerCompaction(state, config, {
|
|
7000
|
+
triggerCompaction(this.state, this.config, {
|
|
6698
7001
|
onStart: () => {
|
|
6699
|
-
sessionStats.compactionInProgress = true;
|
|
6700
|
-
|
|
6701
|
-
try {
|
|
6702
|
-
writeFileSync(".remy-stats.json", JSON.stringify(sessionStats));
|
|
6703
|
-
} catch {
|
|
6704
|
-
}
|
|
7002
|
+
this.sessionStats.compactionInProgress = true;
|
|
7003
|
+
this.persistStats();
|
|
6705
7004
|
},
|
|
6706
7005
|
onSummariesReady: () => {
|
|
6707
|
-
if (!running) {
|
|
6708
|
-
applyPendingSummaries();
|
|
7006
|
+
if (!this.running) {
|
|
7007
|
+
this.applyPendingSummaries();
|
|
6709
7008
|
}
|
|
6710
|
-
emit("compaction_complete", {}, requestId);
|
|
6711
|
-
emit("completed", { success: true }, requestId);
|
|
7009
|
+
this.emit("compaction_complete", {}, requestId);
|
|
7010
|
+
this.emit("completed", { success: true }, requestId);
|
|
6712
7011
|
},
|
|
6713
7012
|
onError: (error) => {
|
|
6714
|
-
emit("compaction_complete", { error }, requestId);
|
|
6715
|
-
emit("completed", { success: false, error }, requestId);
|
|
7013
|
+
this.emit("compaction_complete", { error }, requestId);
|
|
7014
|
+
this.emit("completed", { success: false, error }, requestId);
|
|
6716
7015
|
},
|
|
6717
7016
|
onFinally: () => {
|
|
6718
|
-
sessionStats.compactionInProgress = false;
|
|
6719
|
-
sessionStats.lastContextSize = 0;
|
|
6720
|
-
sessionStats.messageCount = state.messages.length;
|
|
6721
|
-
|
|
6722
|
-
try {
|
|
6723
|
-
writeFileSync(".remy-stats.json", JSON.stringify(sessionStats));
|
|
6724
|
-
} catch {
|
|
6725
|
-
}
|
|
7017
|
+
this.sessionStats.compactionInProgress = false;
|
|
7018
|
+
this.sessionStats.lastContextSize = 0;
|
|
7019
|
+
this.sessionStats.messageCount = this.state.messages.length;
|
|
7020
|
+
this.persistStats();
|
|
6726
7021
|
}
|
|
6727
7022
|
});
|
|
6728
7023
|
return;
|
|
6729
7024
|
}
|
|
6730
7025
|
if (action === "message") {
|
|
6731
|
-
await handleMessage(parsed, requestId);
|
|
7026
|
+
await this.handleMessage(parsed, requestId);
|
|
6732
7027
|
return;
|
|
6733
7028
|
}
|
|
6734
|
-
|
|
6735
|
-
|
|
7029
|
+
if (action === "resume") {
|
|
7030
|
+
if (this.running) {
|
|
7031
|
+
this.emit(
|
|
7032
|
+
"completed",
|
|
7033
|
+
{ success: false, error: "already running" },
|
|
7034
|
+
requestId
|
|
7035
|
+
);
|
|
7036
|
+
return;
|
|
7037
|
+
}
|
|
7038
|
+
if (this.queue.length === 0) {
|
|
7039
|
+
this.emit("completed", { success: true }, requestId);
|
|
7040
|
+
return;
|
|
7041
|
+
}
|
|
7042
|
+
this.emit("completed", { success: true }, requestId);
|
|
7043
|
+
await this.resumeQueue();
|
|
7044
|
+
return;
|
|
7045
|
+
}
|
|
7046
|
+
this.emit("error", { error: `Unknown action: ${action}` }, requestId);
|
|
7047
|
+
this.emit(
|
|
6736
7048
|
"completed",
|
|
6737
7049
|
{ success: false, error: `Unknown action: ${action}` },
|
|
6738
7050
|
requestId
|
|
6739
7051
|
);
|
|
6740
|
-
}
|
|
6741
|
-
|
|
6742
|
-
emit("stopping");
|
|
6743
|
-
emit("stopped");
|
|
6744
|
-
process.exit(0);
|
|
6745
|
-
});
|
|
6746
|
-
function shutdown() {
|
|
6747
|
-
emit("stopping");
|
|
6748
|
-
emit("stopped");
|
|
6749
|
-
process.exit(0);
|
|
6750
|
-
}
|
|
6751
|
-
process.on("SIGTERM", shutdown);
|
|
6752
|
-
process.on("SIGINT", shutdown);
|
|
6753
|
-
emit("ready");
|
|
6754
|
-
}
|
|
7052
|
+
};
|
|
7053
|
+
};
|
|
6755
7054
|
export {
|
|
6756
|
-
|
|
7055
|
+
HeadlessSession
|
|
6757
7056
|
};
|