@mindstudio-ai/remy 0.1.330 → 0.1.332
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/README.md +3 -4
- package/dist/headless.d.ts +22 -0
- package/dist/headless.js +270 -26
- package/dist/index.js +281 -27
- package/dist/prompt/compiled/dev-and-deploy.md +9 -0
- package/dist/prompt/compiled/platform.md +1 -1
- package/dist/prompt/skills/dataSources.md +11 -2
- package/dist/prompt/skills/publishing.md +60 -6
- package/dist/prompt/static/instructions.md +4 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -172,6 +172,10 @@ var init_loopGuard = __esm({
|
|
|
172
172
|
});
|
|
173
173
|
|
|
174
174
|
// src/api.ts
|
|
175
|
+
function sandboxSessionHeader() {
|
|
176
|
+
const sessionId = process.env.MINDSTUDIO_SESSION_ID;
|
|
177
|
+
return sessionId ? { "x-sandbox-session": sessionId } : {};
|
|
178
|
+
}
|
|
175
179
|
async function* streamChat(params) {
|
|
176
180
|
const { baseUrl: baseUrl2, apiKey, signal, requestId, model, ...rest } = params;
|
|
177
181
|
const url = `${baseUrl2}/_internal/v2/agent/remy/chat`;
|
|
@@ -191,7 +195,8 @@ async function* streamChat(params) {
|
|
|
191
195
|
method: "POST",
|
|
192
196
|
headers: {
|
|
193
197
|
"Content-Type": "application/json",
|
|
194
|
-
Authorization: `Bearer ${apiKey}
|
|
198
|
+
Authorization: `Bearer ${apiKey}`,
|
|
199
|
+
...sandboxSessionHeader()
|
|
195
200
|
},
|
|
196
201
|
body: JSON.stringify(requestBody),
|
|
197
202
|
signal
|
|
@@ -446,7 +451,8 @@ async function generateBackgroundAck(params) {
|
|
|
446
451
|
method: "POST",
|
|
447
452
|
headers: {
|
|
448
453
|
"Content-Type": "application/json",
|
|
449
|
-
Authorization: `Bearer ${params.apiConfig.apiKey}
|
|
454
|
+
Authorization: `Bearer ${params.apiConfig.apiKey}`,
|
|
455
|
+
...sandboxSessionHeader()
|
|
450
456
|
},
|
|
451
457
|
body: JSON.stringify({
|
|
452
458
|
appId: params.apiConfig.appId,
|
|
@@ -1647,6 +1653,33 @@ ${xml}
|
|
|
1647
1653
|
</background_results>`;
|
|
1648
1654
|
return automatedMessage("background_results", body);
|
|
1649
1655
|
}
|
|
1656
|
+
function buildWorkspaceStatusMessage(status) {
|
|
1657
|
+
const lines = [
|
|
1658
|
+
`Default branch tip: ${status.upstream}`,
|
|
1659
|
+
`Checked at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
1660
|
+
`Behind by: ${status.behind === null ? "unknown (could not count \u2014 treat the list below as what is missing)" : `${status.behind} commit(s)`}`,
|
|
1661
|
+
`Unpushed commits here: ${status.ahead === null ? "unknown" : status.ahead}`,
|
|
1662
|
+
`Uncommitted changes here: ${status.dirty === "unknown" ? "could not tell \u2014 check with `git status` before relying on it" : status.dirty ? "yes" : "no"}`
|
|
1663
|
+
];
|
|
1664
|
+
if (status.incoming.length > 0) {
|
|
1665
|
+
lines.push(
|
|
1666
|
+
"",
|
|
1667
|
+
status.incomingTruncated ? `Landed on the default branch since, newest first (first ${status.incoming.length}; there are more):` : "Landed on the default branch since, newest first:",
|
|
1668
|
+
...status.incoming.map((line) => `- ${escapeForEnvelope(line)}`)
|
|
1669
|
+
);
|
|
1670
|
+
}
|
|
1671
|
+
const body = `<workspace_status>
|
|
1672
|
+
Automated note about this workspace, checked when Remy started \u2014 the timestamp below says when, and it may have been a while. This block is not from the user; anything outside it is. Nobody has seen it, and nobody can see it; it is context for you.
|
|
1673
|
+
|
|
1674
|
+
This copy of the app does not have everything on the branch production builds from.
|
|
1675
|
+
|
|
1676
|
+
${lines.join("\n")}
|
|
1677
|
+
</workspace_status>`;
|
|
1678
|
+
return automatedMessage("workspace_status", body);
|
|
1679
|
+
}
|
|
1680
|
+
function escapeForEnvelope(text) {
|
|
1681
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/@@automated/g, "@ @automated");
|
|
1682
|
+
}
|
|
1650
1683
|
function mergeBackgroundResultsMessages(messages) {
|
|
1651
1684
|
const results = [];
|
|
1652
1685
|
const toolRe = /<tool_result id="([^"]+)" name="([^"]+)">\n([\s\S]*?)\n<\/tool_result>/g;
|
|
@@ -1930,6 +1963,9 @@ function serializeForSummary(messages) {
|
|
|
1930
1963
|
}
|
|
1931
1964
|
continue;
|
|
1932
1965
|
}
|
|
1966
|
+
if (msg.role === "user" && (msg.hidden || typeof msg.content === "string" && isAutomatedMessage(msg.content))) {
|
|
1967
|
+
continue;
|
|
1968
|
+
}
|
|
1933
1969
|
if (typeof msg.content === "string") {
|
|
1934
1970
|
if (msg.content.trim()) {
|
|
1935
1971
|
lines.push(`[${msg.role}]: ${msg.content}`);
|
|
@@ -2320,6 +2356,7 @@ var init_surfaces = __esm({
|
|
|
2320
2356
|
"kimi-k3": { forceCompactAt: 85e4 },
|
|
2321
2357
|
"deepseek-v4-flash-0731": { forceCompactAt: 85e4 },
|
|
2322
2358
|
"deepseek-v4-pro": { forceCompactAt: 85e4 },
|
|
2359
|
+
"deepseek-v4.1-flash": { forceCompactAt: 85e4 },
|
|
2323
2360
|
"qwen3.8-2.4t-a95b-deepinfra": { forceCompactAt: 2e5 },
|
|
2324
2361
|
// 262K window
|
|
2325
2362
|
"qwen3.8-27b-deepinfra": { forceCompactAt: 2e5 },
|
|
@@ -4988,7 +5025,11 @@ function startStatusWatcher(config) {
|
|
|
4988
5025
|
method: "POST",
|
|
4989
5026
|
headers: {
|
|
4990
5027
|
"Content-Type": "application/json",
|
|
4991
|
-
Authorization: `Bearer ${apiConfig.apiKey}
|
|
5028
|
+
Authorization: `Bearer ${apiConfig.apiKey}`,
|
|
5029
|
+
// Also a liveness signal for this box, and the most frequent one there is — this ticks
|
|
5030
|
+
// every few seconds for as long as the agent is working, which is exactly the window in
|
|
5031
|
+
// which the user may have backgrounded the tab and stopped its own keepalive.
|
|
5032
|
+
...sandboxSessionHeader()
|
|
4992
5033
|
},
|
|
4993
5034
|
body: JSON.stringify({ appId: apiConfig.appId, context }),
|
|
4994
5035
|
signal
|
|
@@ -5040,11 +5081,13 @@ var INTERNAL_PAYLOAD_MARKERS;
|
|
|
5040
5081
|
var init_statusWatcher = __esm({
|
|
5041
5082
|
"src/statusWatcher.ts"() {
|
|
5042
5083
|
"use strict";
|
|
5084
|
+
init_api();
|
|
5043
5085
|
INTERNAL_PAYLOAD_MARKERS = [
|
|
5044
5086
|
"[USER CANCELLED]",
|
|
5045
5087
|
"[INTERRUPTED]",
|
|
5046
5088
|
"[INTERRUPTED - PARTIAL OUTPUT RETRIEVED]",
|
|
5047
5089
|
"<background_results>",
|
|
5090
|
+
"<workspace_status>",
|
|
5048
5091
|
"<tool_result"
|
|
5049
5092
|
];
|
|
5050
5093
|
}
|
|
@@ -5894,7 +5937,7 @@ TypeScript running in a sandboxed environment. Any npm package can be installed.
|
|
|
5894
5937
|
- Managed SQLite database with typed schemas and automatic migrations. Define a TypeScript interface, push, and the platform handles diffing and migrating.
|
|
5895
5938
|
- 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.
|
|
5896
5939
|
- Encrypted secrets with separate dev/prod values, injected as process.env. For third-party service credentials not covered by the SDK.
|
|
5897
|
-
- Git-native deployment.
|
|
5940
|
+
- Git-native deployment. Each person with edit access works in their own copy of the app, and everyone publishes to the same default branch, which is what deploys. Pushing any other branch builds a private preview instead. Rollback is a git revert.
|
|
5898
5941
|
|
|
5899
5942
|
## MindStudio SDK
|
|
5900
5943
|
|
|
@@ -9435,7 +9478,10 @@ var init_resolve = __esm({
|
|
|
9435
9478
|
"use strict";
|
|
9436
9479
|
init_assets();
|
|
9437
9480
|
init_sentinel();
|
|
9438
|
-
NON_ACTION_SENTINELS = /* @__PURE__ */ new Set([
|
|
9481
|
+
NON_ACTION_SENTINELS = /* @__PURE__ */ new Set([
|
|
9482
|
+
"background_results",
|
|
9483
|
+
"workspace_status"
|
|
9484
|
+
]);
|
|
9439
9485
|
}
|
|
9440
9486
|
});
|
|
9441
9487
|
|
|
@@ -10111,7 +10157,9 @@ async function runTurn(params) {
|
|
|
10111
10157
|
for (const entry of keptEntries) {
|
|
10112
10158
|
appendEntry(entry);
|
|
10113
10159
|
}
|
|
10114
|
-
const isFirstMessage = state.messages.filter(
|
|
10160
|
+
const isFirstMessage = state.messages.filter(
|
|
10161
|
+
(m) => m.role === "user" && !m.hidden && !(typeof m.content === "string" && isAutomatedMessage(m.content))
|
|
10162
|
+
).length === 1;
|
|
10115
10163
|
const STATUS_EXCLUDED_TOOLS = /* @__PURE__ */ new Set([
|
|
10116
10164
|
"markBuildComplete",
|
|
10117
10165
|
"setProjectMetadata",
|
|
@@ -11310,7 +11358,24 @@ function loadPassiveResults() {
|
|
|
11310
11358
|
}
|
|
11311
11359
|
return [];
|
|
11312
11360
|
}
|
|
11313
|
-
function
|
|
11361
|
+
function emptyWorkspaceNotice() {
|
|
11362
|
+
return { pendingNote: null, lastNotedUpstream: null };
|
|
11363
|
+
}
|
|
11364
|
+
function loadWorkspaceNotice() {
|
|
11365
|
+
try {
|
|
11366
|
+
const stats = JSON.parse(readFileSync2(STATS_FILE, "utf-8"));
|
|
11367
|
+
const notice = stats.workspaceNotice;
|
|
11368
|
+
if (notice && typeof notice === "object") {
|
|
11369
|
+
return {
|
|
11370
|
+
pendingNote: typeof notice.pendingNote === "string" ? notice.pendingNote : null,
|
|
11371
|
+
lastNotedUpstream: typeof notice.lastNotedUpstream === "string" ? notice.lastNotedUpstream : null
|
|
11372
|
+
};
|
|
11373
|
+
}
|
|
11374
|
+
} catch {
|
|
11375
|
+
}
|
|
11376
|
+
return emptyWorkspaceNotice();
|
|
11377
|
+
}
|
|
11378
|
+
function writeStats(stats, queue, passiveResults, suggestCompactAt, workspaceNotice) {
|
|
11314
11379
|
try {
|
|
11315
11380
|
writeFileAtomicSync(
|
|
11316
11381
|
STATS_FILE,
|
|
@@ -11318,7 +11383,8 @@ function writeStats(stats, queue, passiveResults, suggestCompactAt) {
|
|
|
11318
11383
|
...stats,
|
|
11319
11384
|
suggestCompactAt,
|
|
11320
11385
|
queue,
|
|
11321
|
-
passiveResults
|
|
11386
|
+
passiveResults,
|
|
11387
|
+
workspaceNotice
|
|
11322
11388
|
})
|
|
11323
11389
|
);
|
|
11324
11390
|
} catch {
|
|
@@ -11333,6 +11399,134 @@ var init_stats = __esm({
|
|
|
11333
11399
|
}
|
|
11334
11400
|
});
|
|
11335
11401
|
|
|
11402
|
+
// src/git/upstreamStatus.ts
|
|
11403
|
+
import { execFile as execFile2 } from "child_process";
|
|
11404
|
+
function git(args2, timeout = GIT_TIMEOUT_MS) {
|
|
11405
|
+
return new Promise((resolve4) => {
|
|
11406
|
+
const child = execFile2(
|
|
11407
|
+
"git",
|
|
11408
|
+
args2,
|
|
11409
|
+
{
|
|
11410
|
+
cwd: PROJECT_ROOT,
|
|
11411
|
+
encoding: "utf-8",
|
|
11412
|
+
timeout,
|
|
11413
|
+
maxBuffer: MAX_BUFFER_BYTES,
|
|
11414
|
+
// No controlling tty in a box, but an inherited one anywhere else would
|
|
11415
|
+
// let a credential prompt hold the whole timeout open.
|
|
11416
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
|
|
11417
|
+
// execFile's `timeout` sends SIGTERM, which a wedged `git-remote-https`
|
|
11418
|
+
// can ignore. Escalate so the boot path cannot be held by one.
|
|
11419
|
+
killSignal: "SIGKILL"
|
|
11420
|
+
},
|
|
11421
|
+
(err, stdout, stderr) => {
|
|
11422
|
+
resolve4({
|
|
11423
|
+
ok: !err,
|
|
11424
|
+
stdout: (stdout ?? "").trim(),
|
|
11425
|
+
// Kept so a failure can say WHY. For a probe whose entire failure
|
|
11426
|
+
// model is silence, this is the only line that makes it debuggable.
|
|
11427
|
+
error: err ? (stderr || "").trim() || err.message : null
|
|
11428
|
+
});
|
|
11429
|
+
}
|
|
11430
|
+
);
|
|
11431
|
+
child.on("error", () => {
|
|
11432
|
+
});
|
|
11433
|
+
});
|
|
11434
|
+
}
|
|
11435
|
+
async function readUpstreamStatus() {
|
|
11436
|
+
const inRepo = await git(["rev-parse", "--git-dir"]);
|
|
11437
|
+
if (!inRepo.ok) {
|
|
11438
|
+
return null;
|
|
11439
|
+
}
|
|
11440
|
+
const fetched = await git(
|
|
11441
|
+
["fetch", "origin", DEFAULT_BRANCH],
|
|
11442
|
+
FETCH_TIMEOUT_MS
|
|
11443
|
+
);
|
|
11444
|
+
if (!fetched.ok) {
|
|
11445
|
+
log17.info(
|
|
11446
|
+
`fetch failed, falling back to the origin/${DEFAULT_BRANCH} on disk: ${fetched.error}`
|
|
11447
|
+
);
|
|
11448
|
+
}
|
|
11449
|
+
const [upstreamRef, headRef] = await Promise.all([
|
|
11450
|
+
git(["rev-parse", `origin/${DEFAULT_BRANCH}`]),
|
|
11451
|
+
git(["rev-parse", "HEAD"])
|
|
11452
|
+
]);
|
|
11453
|
+
if (!upstreamRef.ok || !headRef.ok) {
|
|
11454
|
+
return null;
|
|
11455
|
+
}
|
|
11456
|
+
const upstream = upstreamRef.stdout;
|
|
11457
|
+
if (upstream === headRef.stdout) {
|
|
11458
|
+
return null;
|
|
11459
|
+
}
|
|
11460
|
+
const incomingResult = await git([
|
|
11461
|
+
"log",
|
|
11462
|
+
`--max-count=${MAX_INCOMING + 1}`,
|
|
11463
|
+
"--format=%an: %s",
|
|
11464
|
+
`HEAD..origin/${DEFAULT_BRANCH}`
|
|
11465
|
+
]);
|
|
11466
|
+
const incomingLines = incomingResult.ok ? incomingResult.stdout.split("\n").filter((line) => line.length > 0) : [];
|
|
11467
|
+
if (incomingResult.ok && incomingLines.length === 0) {
|
|
11468
|
+
return null;
|
|
11469
|
+
}
|
|
11470
|
+
let behind = null;
|
|
11471
|
+
let ahead = null;
|
|
11472
|
+
const counts = await git([
|
|
11473
|
+
"rev-list",
|
|
11474
|
+
"--left-right",
|
|
11475
|
+
"--count",
|
|
11476
|
+
`origin/${DEFAULT_BRANCH}...HEAD`
|
|
11477
|
+
]);
|
|
11478
|
+
if (counts.ok) {
|
|
11479
|
+
const [left, right] = counts.stdout.split(/\s+/);
|
|
11480
|
+
behind = Number(left);
|
|
11481
|
+
ahead = Number(right);
|
|
11482
|
+
if (!Number.isFinite(behind) || behind <= 0) {
|
|
11483
|
+
return null;
|
|
11484
|
+
}
|
|
11485
|
+
if (!Number.isFinite(ahead)) {
|
|
11486
|
+
ahead = null;
|
|
11487
|
+
}
|
|
11488
|
+
} else if (!incomingResult.ok) {
|
|
11489
|
+
const contained = await git([
|
|
11490
|
+
"merge-base",
|
|
11491
|
+
"--is-ancestor",
|
|
11492
|
+
`origin/${DEFAULT_BRANCH}`,
|
|
11493
|
+
"HEAD"
|
|
11494
|
+
]);
|
|
11495
|
+
if (contained.ok) {
|
|
11496
|
+
return null;
|
|
11497
|
+
}
|
|
11498
|
+
log17.info(
|
|
11499
|
+
`behind by an unknown amount: rev-list and log both failed (${counts.error})`
|
|
11500
|
+
);
|
|
11501
|
+
}
|
|
11502
|
+
const status = await git(["status", "--porcelain"]);
|
|
11503
|
+
if (!status.ok) {
|
|
11504
|
+
log17.info(`could not read the working tree state: ${status.error}`);
|
|
11505
|
+
}
|
|
11506
|
+
return {
|
|
11507
|
+
upstream,
|
|
11508
|
+
behind,
|
|
11509
|
+
ahead,
|
|
11510
|
+
dirty: status.ok ? status.stdout.length > 0 : "unknown",
|
|
11511
|
+
incoming: incomingLines.slice(0, MAX_INCOMING),
|
|
11512
|
+
incomingTruncated: incomingLines.length > MAX_INCOMING
|
|
11513
|
+
};
|
|
11514
|
+
}
|
|
11515
|
+
var log17, DEFAULT_BRANCH, MAX_INCOMING, FETCH_TIMEOUT_MS, GIT_TIMEOUT_MS, MAX_BUFFER_BYTES;
|
|
11516
|
+
var init_upstreamStatus = __esm({
|
|
11517
|
+
"src/git/upstreamStatus.ts"() {
|
|
11518
|
+
"use strict";
|
|
11519
|
+
init_projectRoot();
|
|
11520
|
+
init_logger();
|
|
11521
|
+
log17 = createLogger("upstream");
|
|
11522
|
+
DEFAULT_BRANCH = "main";
|
|
11523
|
+
MAX_INCOMING = 20;
|
|
11524
|
+
FETCH_TIMEOUT_MS = 3e4;
|
|
11525
|
+
GIT_TIMEOUT_MS = 1e4;
|
|
11526
|
+
MAX_BUFFER_BYTES = 32 * 1024 * 1024;
|
|
11527
|
+
}
|
|
11528
|
+
});
|
|
11529
|
+
|
|
11336
11530
|
// src/headless/messageQueue.ts
|
|
11337
11531
|
function holdRestoredUserItems(items) {
|
|
11338
11532
|
return items.map(
|
|
@@ -11549,7 +11743,7 @@ var headless_exports = {};
|
|
|
11549
11743
|
__export(headless_exports, {
|
|
11550
11744
|
HeadlessSession: () => HeadlessSession
|
|
11551
11745
|
});
|
|
11552
|
-
var
|
|
11746
|
+
var log18, EXTERNAL_TOOL_TIMEOUT_MS, LONG_RUNNING_TOOLS, LONG_RUNNING_TOOL_TIMEOUT_MS, USER_FACING_TOOLS, HeadlessSession;
|
|
11553
11747
|
var init_headless = __esm({
|
|
11554
11748
|
"src/headless/index.ts"() {
|
|
11555
11749
|
"use strict";
|
|
@@ -11567,11 +11761,12 @@ var init_headless = __esm({
|
|
|
11567
11761
|
init_attachments();
|
|
11568
11762
|
init_planFile();
|
|
11569
11763
|
init_stats();
|
|
11764
|
+
init_upstreamStatus();
|
|
11570
11765
|
init_tools10();
|
|
11571
11766
|
init_messageQueue();
|
|
11572
11767
|
init_resolve();
|
|
11573
11768
|
init_sentinel();
|
|
11574
|
-
|
|
11769
|
+
log18 = createLogger("headless");
|
|
11575
11770
|
EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
|
|
11576
11771
|
LONG_RUNNING_TOOLS = /* @__PURE__ */ new Set(["runMethod", "testJewel"]);
|
|
11577
11772
|
LONG_RUNNING_TOOL_TIMEOUT_MS = 18e5;
|
|
@@ -11635,6 +11830,15 @@ var init_headless = __esm({
|
|
|
11635
11830
|
* Persisted to .remy-stats.json alongside the queue.
|
|
11636
11831
|
*/
|
|
11637
11832
|
passivePen = [];
|
|
11833
|
+
/**
|
|
11834
|
+
* The workspace-behind note, and the upstream tip it was raised for.
|
|
11835
|
+
*
|
|
11836
|
+
* Rides the same sweep as the passive pen and for the same reason: a
|
|
11837
|
+
* workspace that is behind is worth knowing about before the next piece of
|
|
11838
|
+
* work, and worth nothing at all if nobody is working — so it must never
|
|
11839
|
+
* initiate a turn of its own.
|
|
11840
|
+
*/
|
|
11841
|
+
workspaceNotice = emptyWorkspaceNotice();
|
|
11638
11842
|
// External tool bridge
|
|
11639
11843
|
pendingTools = /* @__PURE__ */ new Map();
|
|
11640
11844
|
earlyResults = /* @__PURE__ */ new Map();
|
|
@@ -11681,6 +11885,8 @@ var init_headless = __esm({
|
|
|
11681
11885
|
}
|
|
11682
11886
|
);
|
|
11683
11887
|
this.passivePen = loadPassiveResults();
|
|
11888
|
+
this.workspaceNotice = loadWorkspaceNotice();
|
|
11889
|
+
void this.checkUpstream();
|
|
11684
11890
|
this.persistStats();
|
|
11685
11891
|
if (resumed) {
|
|
11686
11892
|
this.emit("session_restored", {
|
|
@@ -11804,7 +12010,7 @@ var init_headless = __esm({
|
|
|
11804
12010
|
try {
|
|
11805
12011
|
this.handleCancel("shutdown");
|
|
11806
12012
|
} catch (err) {
|
|
11807
|
-
|
|
12013
|
+
log18.warn("Shutdown cancel failed", { error: err?.message });
|
|
11808
12014
|
}
|
|
11809
12015
|
this.emit("stopping");
|
|
11810
12016
|
this.emit("stopped");
|
|
@@ -11820,7 +12026,7 @@ var init_headless = __esm({
|
|
|
11820
12026
|
}
|
|
11821
12027
|
const line = JSON.stringify(payload) + "\n";
|
|
11822
12028
|
if (event === "history") {
|
|
11823
|
-
|
|
12029
|
+
log18.info("Wrote history event to stdout", {
|
|
11824
12030
|
requestId,
|
|
11825
12031
|
bytes: line.length
|
|
11826
12032
|
});
|
|
@@ -11867,9 +12073,45 @@ var init_headless = __esm({
|
|
|
11867
12073
|
this.sessionStats,
|
|
11868
12074
|
this.queue.snapshot(),
|
|
11869
12075
|
this.passivePen,
|
|
11870
|
-
suggestCompactAt
|
|
12076
|
+
suggestCompactAt,
|
|
12077
|
+
this.workspaceNotice
|
|
11871
12078
|
);
|
|
11872
12079
|
}
|
|
12080
|
+
/**
|
|
12081
|
+
* Park a note if this workspace is missing work that is already in production.
|
|
12082
|
+
*
|
|
12083
|
+
* Once per upstream tip rather than once per boot. Remy restarts for reasons
|
|
12084
|
+
* that have nothing to do with the repo — a pod recycle, a crash, a new
|
|
12085
|
+
* session — and re-raising the same note on each of those turns a useful
|
|
12086
|
+
* signal into nagging. A colleague publishing again moves the tip, which
|
|
12087
|
+
* re-arms it; bringing the workspace current means there is nothing to raise.
|
|
12088
|
+
*
|
|
12089
|
+
* Never throws: this is called unawaited from the boot path, so an unhandled
|
|
12090
|
+
* rejection here would be a crash at the least recoverable moment.
|
|
12091
|
+
*/
|
|
12092
|
+
async checkUpstream() {
|
|
12093
|
+
try {
|
|
12094
|
+
const status = await readUpstreamStatus();
|
|
12095
|
+
if (!status || status.upstream === this.workspaceNotice.lastNotedUpstream) {
|
|
12096
|
+
return;
|
|
12097
|
+
}
|
|
12098
|
+
this.workspaceNotice = {
|
|
12099
|
+
pendingNote: buildWorkspaceStatusMessage(status),
|
|
12100
|
+
lastNotedUpstream: status.upstream
|
|
12101
|
+
};
|
|
12102
|
+
this.persistStats();
|
|
12103
|
+
log18.info("workspace behind upstream; note parked for the next turn", {
|
|
12104
|
+
// The upstream sha is the dedupe key, so it is what makes "why did I
|
|
12105
|
+
// not get a note" answerable from the log alone.
|
|
12106
|
+
upstream: status.upstream,
|
|
12107
|
+
behind: status.behind,
|
|
12108
|
+
ahead: status.ahead,
|
|
12109
|
+
dirty: status.dirty
|
|
12110
|
+
});
|
|
12111
|
+
} catch (err) {
|
|
12112
|
+
log18.info(`upstream check failed: ${String(err)}`);
|
|
12113
|
+
}
|
|
12114
|
+
}
|
|
11873
12115
|
//////////////////////////////////////////////////////////////////////////////
|
|
11874
12116
|
// Background completions (tool-block mutation; message delivery via queue)
|
|
11875
12117
|
//////////////////////////////////////////////////////////////////////////////
|
|
@@ -11916,7 +12158,7 @@ var init_headless = __esm({
|
|
|
11916
12158
|
if (this.sessionStats.lastContextSize <= threshold) {
|
|
11917
12159
|
return;
|
|
11918
12160
|
}
|
|
11919
|
-
|
|
12161
|
+
log18.info("Forced compaction gate triggered", {
|
|
11920
12162
|
contextSize: this.sessionStats.lastContextSize,
|
|
11921
12163
|
threshold,
|
|
11922
12164
|
model: parentModel,
|
|
@@ -11936,7 +12178,7 @@ var init_headless = __esm({
|
|
|
11936
12178
|
onBackgroundComplete = (toolCallId, name, result, subAgentMessages) => {
|
|
11937
12179
|
const notify = getToolByName(name)?.backgroundNotify ?? "wake";
|
|
11938
12180
|
this.pendingBlockUpdates.push({ toolCallId, result, subAgentMessages });
|
|
11939
|
-
|
|
12181
|
+
log18.info("Background complete", {
|
|
11940
12182
|
toolCallId,
|
|
11941
12183
|
name,
|
|
11942
12184
|
notify,
|
|
@@ -12218,7 +12460,7 @@ var init_headless = __esm({
|
|
|
12218
12460
|
const { documents, images } = await persistAttachments(attachments);
|
|
12219
12461
|
return buildUploadHeader(documents, images) || void 0;
|
|
12220
12462
|
} catch (err) {
|
|
12221
|
-
|
|
12463
|
+
log18.warn("Attachment persistence failed", { error: err.message });
|
|
12222
12464
|
return void 0;
|
|
12223
12465
|
}
|
|
12224
12466
|
}
|
|
@@ -12278,7 +12520,7 @@ var init_headless = __esm({
|
|
|
12278
12520
|
}
|
|
12279
12521
|
if (batch.length === 0) {
|
|
12280
12522
|
if (landings > 0) {
|
|
12281
|
-
|
|
12523
|
+
log18.info("promptUser store landings passed through", { landings });
|
|
12282
12524
|
}
|
|
12283
12525
|
return raw;
|
|
12284
12526
|
}
|
|
@@ -12286,7 +12528,7 @@ var init_headless = __esm({
|
|
|
12286
12528
|
try {
|
|
12287
12529
|
results = await persistAttachmentList(batch);
|
|
12288
12530
|
} catch (err) {
|
|
12289
|
-
|
|
12531
|
+
log18.warn("promptUser upload persistence failed", {
|
|
12290
12532
|
error: err.message
|
|
12291
12533
|
});
|
|
12292
12534
|
results = batch.map(() => null);
|
|
@@ -12298,7 +12540,7 @@ var init_headless = __esm({
|
|
|
12298
12540
|
return r.localPath;
|
|
12299
12541
|
}
|
|
12300
12542
|
const att = batch[cursor + i];
|
|
12301
|
-
|
|
12543
|
+
log18.warn("promptUser upload not persisted; falling back to url", {
|
|
12302
12544
|
filename: att.filename
|
|
12303
12545
|
});
|
|
12304
12546
|
return att.url;
|
|
@@ -12306,7 +12548,7 @@ var init_headless = __esm({
|
|
|
12306
12548
|
cursor += slot.count;
|
|
12307
12549
|
answers[slot.id] = slot.isArray ? paths : paths[0];
|
|
12308
12550
|
}
|
|
12309
|
-
|
|
12551
|
+
log18.info("promptUser uploads persisted", { count: batch.length });
|
|
12310
12552
|
return JSON.stringify(answers);
|
|
12311
12553
|
}
|
|
12312
12554
|
/**
|
|
@@ -12329,7 +12571,7 @@ var init_headless = __esm({
|
|
|
12329
12571
|
async runSingleTurn(parsed, requestId, fromChain = false, queued = false) {
|
|
12330
12572
|
const attachments = parsed.attachments;
|
|
12331
12573
|
if (attachments?.length) {
|
|
12332
|
-
|
|
12574
|
+
log18.info("Message has attachments", {
|
|
12333
12575
|
count: attachments.length,
|
|
12334
12576
|
urls: attachments.map((a) => a.url)
|
|
12335
12577
|
});
|
|
@@ -12477,6 +12719,18 @@ var init_headless = __esm({
|
|
|
12477
12719
|
hidden: true
|
|
12478
12720
|
});
|
|
12479
12721
|
}
|
|
12722
|
+
const hasUserWords = entries.some(
|
|
12723
|
+
(entry) => !entry.hidden && !isAutomatedMessage(entry.text)
|
|
12724
|
+
);
|
|
12725
|
+
if (this.workspaceNotice.pendingNote && hasUserWords) {
|
|
12726
|
+
const note = this.workspaceNotice.pendingNote;
|
|
12727
|
+
this.workspaceNotice = {
|
|
12728
|
+
...this.workspaceNotice,
|
|
12729
|
+
pendingNote: null
|
|
12730
|
+
};
|
|
12731
|
+
this.persistStats();
|
|
12732
|
+
entries.unshift({ text: note, hidden: true });
|
|
12733
|
+
}
|
|
12480
12734
|
const consumedRids = [];
|
|
12481
12735
|
const takeSteering = async () => {
|
|
12482
12736
|
const items = this.queue.removeWhere(
|
|
@@ -12529,7 +12783,7 @@ var init_headless = __esm({
|
|
|
12529
12783
|
error: "Turn ended unexpectedly"
|
|
12530
12784
|
});
|
|
12531
12785
|
}
|
|
12532
|
-
|
|
12786
|
+
log18.info("Turn complete", {
|
|
12533
12787
|
requestId,
|
|
12534
12788
|
durationMs: Date.now() - this.turnStart
|
|
12535
12789
|
});
|
|
@@ -12541,7 +12795,7 @@ var init_headless = __esm({
|
|
|
12541
12795
|
error: err.message
|
|
12542
12796
|
});
|
|
12543
12797
|
}
|
|
12544
|
-
|
|
12798
|
+
log18.warn("Command failed", {
|
|
12545
12799
|
action: "message",
|
|
12546
12800
|
requestId,
|
|
12547
12801
|
error: err.message
|
|
@@ -12848,7 +13102,7 @@ var init_headless = __esm({
|
|
|
12848
13102
|
try {
|
|
12849
13103
|
parsed = JSON.parse(line);
|
|
12850
13104
|
} catch (err) {
|
|
12851
|
-
|
|
13105
|
+
log18.warn("Invalid JSON on stdin", {
|
|
12852
13106
|
error: err.message,
|
|
12853
13107
|
lineLength: line.length,
|
|
12854
13108
|
preview: line.slice(0, 200)
|
|
@@ -12857,7 +13111,7 @@ var init_headless = __esm({
|
|
|
12857
13111
|
return;
|
|
12858
13112
|
}
|
|
12859
13113
|
const { action, requestId } = parsed;
|
|
12860
|
-
|
|
13114
|
+
log18.info("Command received", { action, requestId });
|
|
12861
13115
|
if (action === "tool_result" && parsed.id) {
|
|
12862
13116
|
const id = parsed.id;
|
|
12863
13117
|
const result = parsed.result ?? "";
|
|
@@ -12866,7 +13120,7 @@ var init_headless = __esm({
|
|
|
12866
13120
|
this.pendingTools.delete(id);
|
|
12867
13121
|
pending2.resolve(result);
|
|
12868
13122
|
} else if (!this.running) {
|
|
12869
|
-
|
|
13123
|
+
log18.info("Late tool_result while idle, dismissing", { id });
|
|
12870
13124
|
this.emit("completed", { success: true }, requestId);
|
|
12871
13125
|
} else {
|
|
12872
13126
|
this.earlyResults.set(id, result);
|
|
@@ -12879,7 +13133,7 @@ var init_headless = __esm({
|
|
|
12879
13133
|
...typeof parsed.before === "number" ? { before: parsed.before } : {},
|
|
12880
13134
|
...typeof parsed.limit === "number" ? { limit: parsed.limit } : {}
|
|
12881
13135
|
});
|
|
12882
|
-
|
|
13136
|
+
log18.info("History response", {
|
|
12883
13137
|
requestId,
|
|
12884
13138
|
startIndex: page.startIndex,
|
|
12885
13139
|
endIndex: page.endIndex,
|
|
@@ -25,8 +25,17 @@ The dev database is disposable. Experiment freely — there's no risk of breakin
|
|
|
25
25
|
|
|
26
26
|
`console.log`, `console.warn`, and `console.error` in methods are captured and displayed in the terminal. They don't affect the method's return value. Every method execution is logged with full input, output, duration, and error info.
|
|
27
27
|
|
|
28
|
+
## Working Alongside Other People
|
|
29
|
+
|
|
30
|
+
Most apps are single-owner, but sometimes an app can have several people with edit access, and each of them gets their own workspace: a separate copy of the code, its own dev database, its own conversation with you. Nothing one person does is visible to anyone else until it's pushed, and you can't see into the other workspaces. Everyone's copy tracks the same branch - the one production builds from - and publishing is what advances it. If this copy is missing work the default branch already has, it's likely someone else published while this copy was idle
|
|
31
|
+
|
|
32
|
+
Whenever any of this reaches the user, talk about the work and the people, not the plumbing. What someone shipped and what it means for what they're about to do is useful. Commit counts, branch names, and phrases like "clean tree" or "fast-forward" are not — almost nobody holds an accurate model of git, including people who use it daily, and reciting repository state at someone is not the same as telling them what happened.
|
|
33
|
+
|
|
28
34
|
## What Happens on Deploy
|
|
29
35
|
|
|
36
|
+
Publishing pushes `main` — the `publishing` skill has the flow. This section is what the platform
|
|
37
|
+
does once that branch moves:
|
|
38
|
+
|
|
30
39
|
```bash
|
|
31
40
|
git push origin main
|
|
32
41
|
```
|
|
@@ -92,7 +92,7 @@ const { vendor } = await api.approveVendor({ vendorId: '...' });
|
|
|
92
92
|
- **Built-in auth.** Opt-in via manifest. Developer builds login UI, platform handles verification codes (email/SMS), cookie sessions, and role enforcement. Backend methods use `auth.requireRole('admin')` for access control.
|
|
93
93
|
- **Multiple interfaces, one codebase.** Web, API, Cron, Webhook, Email, MCP, Agent, Voice — all invoke the same methods. Methods don't know which interface called them.
|
|
94
94
|
- **Sandboxed execution.** Each method invocation runs in its own isolated execution context with npm packages pre-installed.
|
|
95
|
-
- **Git-native deployment.** Push to default branch to deploy. Push to feature branch for preview. Rollback is a git revert.
|
|
95
|
+
- **Git-native deployment.** Push to default branch to deploy. Push to feature branch for preview. Rollback is a git revert. Each person with edit access works in their own copy of the app, and everyone publishes to the same default branch.
|
|
96
96
|
- **Secrets.** Encrypted environment variables with separate dev/prod values. Injected as `process.env` in methods. For third-party service credentials not covered by the SDK.
|
|
97
97
|
|
|
98
98
|
## Minimum Viable App
|
|
@@ -82,6 +82,15 @@ People are bad at describing their own data, and the more of it they have the wo
|
|
|
82
82
|
|
|
83
83
|
Three rules hold throughout: credentials are app secrets referred to by NAME and never appear in chat or code; nothing that spends is approved or provisioned without the user's explicit yes on the numbers; and dedicated capacity is proposed when a plan asks for it, not before.
|
|
84
84
|
|
|
85
|
+
### Building an initial app (intake) when a user brings a data source
|
|
86
|
+
|
|
87
|
+
When a user shows up with a data source from the first message, prefer the following workflow:
|
|
88
|
+
- Get a feel for the data, using the methods discussed above
|
|
89
|
+
- Then, and perhaps most importantly, understand what it is the user is trying to *do* with the data. Are they building a generic RAG chatbot, or something more interesting? What is important to them - grounding, citations, etc? And why?
|
|
90
|
+
- Vectorized data that does nothing isn't very useful - building the app that will consume it to do something compelling is the important bit.
|
|
91
|
+
- If the data smells truly large (e.g., will require async work, meaningful cost to ingest, or dedicated capacity/planning, etc), focus on putting a small sample of the data in a data source and then focus on building and delivering the MVP.
|
|
92
|
+
- After the MVP is built and the user feels good about it, you can help the user bring in the full data source.
|
|
93
|
+
|
|
85
94
|
## Loading documents — normally at build time, from the CLI
|
|
86
95
|
|
|
87
96
|
```bash
|
|
@@ -124,7 +133,7 @@ remy-admin datasources jobs approve <id> --wait
|
|
|
124
133
|
remy-admin datasources jobs start --source archive --manifest urls.jsonl --limit 200 --approve --wait # a cheap sample first
|
|
125
134
|
```
|
|
126
135
|
|
|
127
|
-
Two gates decide whether a plan can run: the corpus has to fit the source's placement (a shared-pool source over the per-source cap answers `plan_requires_dedicated`; see Dedicated capacity below), and the workspace has to be able to cover the projection (`insufficient_credits`). **Show the user the plan and get an explicit yes before approving** — the plan is the whole point. `--budget <dollars>` pauses the job at a ceiling; `--limit <n>` loads a sample of the corpus to check quality before committing to all of it. Unchanged documents are skipped by content hash, so re-running a job is free. `jobs pause|resume|cancel` are the controls; search works on the partial corpus throughout. One bulk operation per source at a time (`data_source_busy`).
|
|
136
|
+
Two gates decide whether a plan can run: the corpus has to fit the source's placement (a shared-pool source over the per-source cap answers `plan_requires_dedicated`; see Dedicated capacity below), and the workspace has to be able to cover the projection (`insufficient_credits`). **Show the user the plan and get an explicit yes before approving** — the plan is the whole point. `--budget <dollars>` pauses the job at a ceiling; `--limit <n>` loads a sample of the corpus to check quality before committing to all of it. Unchanged documents are skipped by content hash, so re-running a job is free. `jobs pause|resume|cancel` are the controls; search works on the partial corpus throughout. A batch that fails five times stays failed and the job finishes without it; once the cause is fixed, `jobs retry <id>` runs just those batches again and finishes the job. One bulk operation per source at a time (`data_source_busy`); documents stranded by failed batches do not block a move, and `jobs retry` builds them onto wherever the source lives now.
|
|
128
137
|
|
|
129
138
|
## Keeping a corpus in sync with an S3 bucket (connectors)
|
|
130
139
|
|
|
@@ -210,7 +219,7 @@ remy-admin datasources remap --source archive --wait
|
|
|
210
219
|
|
|
211
220
|
On a job, mapping is its own stage. The mapper turns each object into documents; the platform then ingests those documents in parallel batches of fifty across its workers, whatever one object became. So the size of an object does not set the pace, and a bundle of a thousand records is fine; only the number of objects sets how wide the mapping stage itself runs (three huge files map on three workers, the plan says so as a warning). `jobs status` reads "mapping N of M objects" until that stage is through, then counts documents.
|
|
212
221
|
|
|
213
|
-
A mapper runs on the platform, so the platform has to build it. Any push builds it, and a branch push is a private preview build, which is all a mapper needs. `map deploy` then makes that build's mapper the source's active one: jobs, syncs and `add()` run it from then on, whether or not the app has ever been published. Publishing activates the mapper the live release declares —
|
|
222
|
+
A mapper runs on the platform, so the platform has to build it. Any push builds it, and a branch push is a private preview build, which is all a mapper needs. `map deploy` then makes that build's mapper the source's active one: jobs, syncs and `add()` run it from then on, whether or not the app has ever been published. Publishing activates the mapper the *live release* declares — so if you deployed a mapper from a branch build, that code still has to reach `main` for the next release to declare it. Getting it there is the publish flow's job, not a separate step you invent (see the publishing skill). `jobs start` refuses with `mapper_not_deployed` while the dev session declares a mapper that is not yet active, because the job would otherwise load the raw records as documents.
|
|
214
223
|
|
|
215
224
|
`map test --dev` needs the dev session running (`npx mindstudio dev`); it runs the mapper from local source through the tunnel and prints every outcome with markdown previews. The plan of a mapped job records the mapper's outcome mix on its sample; a run whose skip share climbs past twice that pauses with `pauseReason: 'skips'` for a look at the quarantine. `remap` reads the platform's own raw copies — no origin traffic — skips unchanged markdown by hash, and supersedes changed documents, so a metadata tweak on a million-document source costs frames and little else. `externalId` is the identity everything replaces by; choose it deliberately (the record's stable id, never the key of a file that gets rewritten in place).
|
|
216
225
|
|