@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/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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").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. Push to default branch to deploy.
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(["background_results"]);
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((m) => m.role === "user").length === 1;
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 writeStats(stats, queue, passiveResults, suggestCompactAt) {
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 log17, EXTERNAL_TOOL_TIMEOUT_MS, LONG_RUNNING_TOOLS, LONG_RUNNING_TOOL_TIMEOUT_MS, USER_FACING_TOOLS, HeadlessSession;
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
- log17 = createLogger("headless");
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
- log17.warn("Shutdown cancel failed", { error: err?.message });
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
- log17.info("Wrote history event to stdout", {
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
- log17.info("Forced compaction gate triggered", {
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
- log17.info("Background complete", {
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
- log17.warn("Attachment persistence failed", { error: err.message });
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
- log17.info("promptUser store landings passed through", { landings });
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
- log17.warn("promptUser upload persistence failed", {
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
- log17.warn("promptUser upload not persisted; falling back to url", {
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
- log17.info("promptUser uploads persisted", { count: batch.length });
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
- log17.info("Message has attachments", {
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
- log17.info("Turn complete", {
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
- log17.warn("Command failed", {
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
- log17.warn("Invalid JSON on stdin", {
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
- log17.info("Command received", { action, requestId });
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
- log17.info("Late tool_result while idle, dismissing", { id });
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
- log17.info("History response", {
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 — which is the one you deployed, since publishing fast-forwards the default branch to your branch. So there is nothing extra to do at publish time, and nothing to merge by hand: publishing is the merge (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.
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