@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 CHANGED
@@ -279,12 +279,11 @@ Send a user message to the agent.
279
279
 
280
280
  Fields:
281
281
  - `requestId` — caller-provided correlation ID (echoed on all response events)
282
- - `text` — the user message (required unless `runCommand` is set)
282
+ - `text` — the user message
283
283
  - `onboardingState` — the project's onboarding phase, reflected in the system prompt's dynamic tail and plan-status behavior. One of: `intake`, `building`, `buildComplete`, `onboardingFinished` (default: `onboardingFinished`)
284
284
  - `attachments` — array of `{ url, extractedTextUrl? }` for file attachments
285
- - `runCommand` — triggers a built-in action prompt (`"sync"`, `"publish"`, `"buildFromInitialSpec"`)
286
285
 
287
- When `runCommand` is set, the message text is replaced with a built-in prompt and the user message is marked as `hidden` in conversation history (sent to the LLM but not shown in the UI).
286
+ Built-in actions are triggered by sending their sentinel as the `text` — `@@automated::<name>@@` — which resolves to the action's prompt. See `src/automatedActions/`.
288
287
 
289
288
  #### `tool_result`
290
289
 
@@ -302,7 +301,7 @@ Return the full conversation history.
302
301
  {"action": "get_history", "requestId": "r2"}
303
302
  ```
304
303
 
305
- Messages with `hidden: true` were generated by `runCommand` actions and should not be displayed in the UI.
304
+ Messages with `hidden: true` are internal plumbing and must not be displayed: the platform's passive sweeps (background sub-agent results, the workspace-behind note) and the loop guard's nudges to the model. They are sent to the LLM and are not anything a person said, whatever role they carry. Automated actions are *not* hidden — clients render those from their `@@automated::` prefix.
306
305
 
307
306
  The response also includes `queuedMessages` — the current pending-queue snapshot (possibly empty) — so a client can render the queue immediately on connect/reconnect. Live changes after that arrive via `queue_changed`.
308
307
 
@@ -93,6 +93,15 @@ declare class HeadlessSession {
93
93
  * Persisted to .remy-stats.json alongside the queue.
94
94
  */
95
95
  private passivePen;
96
+ /**
97
+ * The workspace-behind note, and the upstream tip it was raised for.
98
+ *
99
+ * Rides the same sweep as the passive pen and for the same reason: a
100
+ * workspace that is behind is worth knowing about before the next piece of
101
+ * work, and worth nothing at all if nobody is working — so it must never
102
+ * initiate a turn of its own.
103
+ */
104
+ private workspaceNotice;
96
105
  private pendingTools;
97
106
  private earlyResults;
98
107
  private pendingBlockUpdates;
@@ -119,6 +128,19 @@ declare class HeadlessSession {
119
128
  private dispatchSimple;
120
129
  /** Persist sessionStats + queue snapshot + passive pen to .remy-stats.json. */
121
130
  private persistStats;
131
+ /**
132
+ * Park a note if this workspace is missing work that is already in production.
133
+ *
134
+ * Once per upstream tip rather than once per boot. Remy restarts for reasons
135
+ * that have nothing to do with the repo — a pod recycle, a crash, a new
136
+ * session — and re-raising the same note on each of those turns a useful
137
+ * signal into nagging. A colleague publishing again moves the tip, which
138
+ * re-arms it; bringing the workspace current means there is nothing to raise.
139
+ *
140
+ * Never throws: this is called unawaited from the boot path, so an unhandled
141
+ * rejection here would be a crash at the least recoverable moment.
142
+ */
143
+ private checkUpstream;
122
144
  /** Apply queued tool block updates to state.messages. Safe to call any time. */
123
145
  private applyPendingBlockUpdates;
124
146
  /**
package/dist/headless.js CHANGED
@@ -183,6 +183,10 @@ var RepetitionDetector = class {
183
183
 
184
184
  // src/api.ts
185
185
  var log2 = createLogger("api");
186
+ function sandboxSessionHeader() {
187
+ const sessionId = process.env.MINDSTUDIO_SESSION_ID;
188
+ return sessionId ? { "x-sandbox-session": sessionId } : {};
189
+ }
186
190
  async function* streamChat(params) {
187
191
  const { baseUrl: baseUrl2, apiKey, signal, requestId, model, ...rest } = params;
188
192
  const url = `${baseUrl2}/_internal/v2/agent/remy/chat`;
@@ -202,7 +206,8 @@ async function* streamChat(params) {
202
206
  method: "POST",
203
207
  headers: {
204
208
  "Content-Type": "application/json",
205
- Authorization: `Bearer ${apiKey}`
209
+ Authorization: `Bearer ${apiKey}`,
210
+ ...sandboxSessionHeader()
206
211
  },
207
212
  body: JSON.stringify(requestBody),
208
213
  signal
@@ -466,7 +471,8 @@ async function generateBackgroundAck(params) {
466
471
  method: "POST",
467
472
  headers: {
468
473
  "Content-Type": "application/json",
469
- Authorization: `Bearer ${params.apiConfig.apiKey}`
474
+ Authorization: `Bearer ${params.apiConfig.apiKey}`,
475
+ ...sandboxSessionHeader()
470
476
  },
471
477
  body: JSON.stringify({
472
478
  appId: params.apiConfig.appId,
@@ -658,6 +664,7 @@ var TEXT_MODELS = {
658
664
  "kimi-k3": { forceCompactAt: 85e4 },
659
665
  "deepseek-v4-flash-0731": { forceCompactAt: 85e4 },
660
666
  "deepseek-v4-pro": { forceCompactAt: 85e4 },
667
+ "deepseek-v4.1-flash": { forceCompactAt: 85e4 },
661
668
  "qwen3.8-2.4t-a95b-deepinfra": { forceCompactAt: 2e5 },
662
669
  // 262K window
663
670
  "qwen3.8-27b-deepinfra": { forceCompactAt: 2e5 },
@@ -1140,6 +1147,33 @@ ${xml}
1140
1147
  </background_results>`;
1141
1148
  return automatedMessage("background_results", body);
1142
1149
  }
1150
+ function buildWorkspaceStatusMessage(status) {
1151
+ const lines = [
1152
+ `Default branch tip: ${status.upstream}`,
1153
+ `Checked at: ${(/* @__PURE__ */ new Date()).toISOString()}`,
1154
+ `Behind by: ${status.behind === null ? "unknown (could not count \u2014 treat the list below as what is missing)" : `${status.behind} commit(s)`}`,
1155
+ `Unpushed commits here: ${status.ahead === null ? "unknown" : status.ahead}`,
1156
+ `Uncommitted changes here: ${status.dirty === "unknown" ? "could not tell \u2014 check with `git status` before relying on it" : status.dirty ? "yes" : "no"}`
1157
+ ];
1158
+ if (status.incoming.length > 0) {
1159
+ lines.push(
1160
+ "",
1161
+ 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:",
1162
+ ...status.incoming.map((line) => `- ${escapeForEnvelope(line)}`)
1163
+ );
1164
+ }
1165
+ const body = `<workspace_status>
1166
+ 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.
1167
+
1168
+ This copy of the app does not have everything on the branch production builds from.
1169
+
1170
+ ${lines.join("\n")}
1171
+ </workspace_status>`;
1172
+ return automatedMessage("workspace_status", body);
1173
+ }
1174
+ function escapeForEnvelope(text) {
1175
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/@@automated/g, "@ @automated");
1176
+ }
1143
1177
  function mergeBackgroundResultsMessages(messages) {
1144
1178
  const results = [];
1145
1179
  const toolRe = /<tool_result id="([^"]+)" name="([^"]+)">\n([\s\S]*?)\n<\/tool_result>/g;
@@ -3759,6 +3793,7 @@ var INTERNAL_PAYLOAD_MARKERS = [
3759
3793
  "[INTERRUPTED]",
3760
3794
  "[INTERRUPTED - PARTIAL OUTPUT RETRIEVED]",
3761
3795
  "<background_results>",
3796
+ "<workspace_status>",
3762
3797
  "<tool_result"
3763
3798
  ];
3764
3799
  function sanitizeStatusText(text) {
@@ -3813,7 +3848,11 @@ function startStatusWatcher(config) {
3813
3848
  method: "POST",
3814
3849
  headers: {
3815
3850
  "Content-Type": "application/json",
3816
- Authorization: `Bearer ${apiConfig.apiKey}`
3851
+ Authorization: `Bearer ${apiConfig.apiKey}`,
3852
+ // Also a liveness signal for this box, and the most frequent one there is — this ticks
3853
+ // every few seconds for as long as the agent is working, which is exactly the window in
3854
+ // which the user may have backgrounded the tab and stopped its own keepalive.
3855
+ ...sandboxSessionHeader()
3817
3856
  },
3818
3857
  body: JSON.stringify({ appId: apiConfig.appId, context }),
3819
3858
  signal
@@ -4830,7 +4869,7 @@ TypeScript running in a sandboxed environment. Any npm package can be installed.
4830
4869
  - Managed SQLite database with typed schemas and automatic migrations. Define a TypeScript interface, push, and the platform handles diffing and migrating.
4831
4870
  - 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.
4832
4871
  - Encrypted secrets with separate dev/prod values, injected as process.env. For third-party service credentials not covered by the SDK.
4833
- - Git-native deployment. Push to default branch to deploy.
4872
+ - 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.
4834
4873
 
4835
4874
  ## MindStudio SDK
4836
4875
 
@@ -7866,6 +7905,9 @@ function serializeForSummary(messages) {
7866
7905
  }
7867
7906
  continue;
7868
7907
  }
7908
+ if (msg.role === "user" && (msg.hidden || typeof msg.content === "string" && isAutomatedMessage(msg.content))) {
7909
+ continue;
7910
+ }
7869
7911
  if (typeof msg.content === "string") {
7870
7912
  if (msg.content.trim()) {
7871
7913
  lines.push(`[${msg.role}]: ${msg.content}`);
@@ -9025,7 +9067,10 @@ function parsePartialJson(jsonString) {
9025
9067
  }
9026
9068
 
9027
9069
  // src/automatedActions/resolve.ts
9028
- var NON_ACTION_SENTINELS = /* @__PURE__ */ new Set(["background_results"]);
9070
+ var NON_ACTION_SENTINELS = /* @__PURE__ */ new Set([
9071
+ "background_results",
9072
+ "workspace_status"
9073
+ ]);
9029
9074
  function resolveAction(text) {
9030
9075
  const parsed = parseSentinel(text);
9031
9076
  if (!parsed) {
@@ -9401,7 +9446,9 @@ async function runTurn(params) {
9401
9446
  for (const entry of keptEntries) {
9402
9447
  appendEntry(entry);
9403
9448
  }
9404
- const isFirstMessage = state.messages.filter((m) => m.role === "user").length === 1;
9449
+ const isFirstMessage = state.messages.filter(
9450
+ (m) => m.role === "user" && !m.hidden && !(typeof m.content === "string" && isAutomatedMessage(m.content))
9451
+ ).length === 1;
9405
9452
  const STATUS_EXCLUDED_TOOLS = /* @__PURE__ */ new Set([
9406
9453
  "markBuildComplete",
9407
9454
  "setProjectMetadata",
@@ -10295,7 +10342,24 @@ function loadPassiveResults() {
10295
10342
  }
10296
10343
  return [];
10297
10344
  }
10298
- function writeStats(stats, queue, passiveResults, suggestCompactAt) {
10345
+ function emptyWorkspaceNotice() {
10346
+ return { pendingNote: null, lastNotedUpstream: null };
10347
+ }
10348
+ function loadWorkspaceNotice() {
10349
+ try {
10350
+ const stats = JSON.parse(readFileSync2(STATS_FILE, "utf-8"));
10351
+ const notice = stats.workspaceNotice;
10352
+ if (notice && typeof notice === "object") {
10353
+ return {
10354
+ pendingNote: typeof notice.pendingNote === "string" ? notice.pendingNote : null,
10355
+ lastNotedUpstream: typeof notice.lastNotedUpstream === "string" ? notice.lastNotedUpstream : null
10356
+ };
10357
+ }
10358
+ } catch {
10359
+ }
10360
+ return emptyWorkspaceNotice();
10361
+ }
10362
+ function writeStats(stats, queue, passiveResults, suggestCompactAt, workspaceNotice) {
10299
10363
  try {
10300
10364
  writeFileAtomicSync(
10301
10365
  STATS_FILE,
@@ -10303,13 +10367,134 @@ function writeStats(stats, queue, passiveResults, suggestCompactAt) {
10303
10367
  ...stats,
10304
10368
  suggestCompactAt,
10305
10369
  queue,
10306
- passiveResults
10370
+ passiveResults,
10371
+ workspaceNotice
10307
10372
  })
10308
10373
  );
10309
10374
  } catch {
10310
10375
  }
10311
10376
  }
10312
10377
 
10378
+ // src/git/upstreamStatus.ts
10379
+ import { execFile as execFile2 } from "child_process";
10380
+ var log17 = createLogger("upstream");
10381
+ var DEFAULT_BRANCH = "main";
10382
+ var MAX_INCOMING = 20;
10383
+ var FETCH_TIMEOUT_MS = 3e4;
10384
+ var GIT_TIMEOUT_MS = 1e4;
10385
+ var MAX_BUFFER_BYTES = 32 * 1024 * 1024;
10386
+ function git(args, timeout = GIT_TIMEOUT_MS) {
10387
+ return new Promise((resolve4) => {
10388
+ const child = execFile2(
10389
+ "git",
10390
+ args,
10391
+ {
10392
+ cwd: PROJECT_ROOT,
10393
+ encoding: "utf-8",
10394
+ timeout,
10395
+ maxBuffer: MAX_BUFFER_BYTES,
10396
+ // No controlling tty in a box, but an inherited one anywhere else would
10397
+ // let a credential prompt hold the whole timeout open.
10398
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
10399
+ // execFile's `timeout` sends SIGTERM, which a wedged `git-remote-https`
10400
+ // can ignore. Escalate so the boot path cannot be held by one.
10401
+ killSignal: "SIGKILL"
10402
+ },
10403
+ (err, stdout, stderr) => {
10404
+ resolve4({
10405
+ ok: !err,
10406
+ stdout: (stdout ?? "").trim(),
10407
+ // Kept so a failure can say WHY. For a probe whose entire failure
10408
+ // model is silence, this is the only line that makes it debuggable.
10409
+ error: err ? (stderr || "").trim() || err.message : null
10410
+ });
10411
+ }
10412
+ );
10413
+ child.on("error", () => {
10414
+ });
10415
+ });
10416
+ }
10417
+ async function readUpstreamStatus() {
10418
+ const inRepo = await git(["rev-parse", "--git-dir"]);
10419
+ if (!inRepo.ok) {
10420
+ return null;
10421
+ }
10422
+ const fetched = await git(
10423
+ ["fetch", "origin", DEFAULT_BRANCH],
10424
+ FETCH_TIMEOUT_MS
10425
+ );
10426
+ if (!fetched.ok) {
10427
+ log17.info(
10428
+ `fetch failed, falling back to the origin/${DEFAULT_BRANCH} on disk: ${fetched.error}`
10429
+ );
10430
+ }
10431
+ const [upstreamRef, headRef] = await Promise.all([
10432
+ git(["rev-parse", `origin/${DEFAULT_BRANCH}`]),
10433
+ git(["rev-parse", "HEAD"])
10434
+ ]);
10435
+ if (!upstreamRef.ok || !headRef.ok) {
10436
+ return null;
10437
+ }
10438
+ const upstream = upstreamRef.stdout;
10439
+ if (upstream === headRef.stdout) {
10440
+ return null;
10441
+ }
10442
+ const incomingResult = await git([
10443
+ "log",
10444
+ `--max-count=${MAX_INCOMING + 1}`,
10445
+ "--format=%an: %s",
10446
+ `HEAD..origin/${DEFAULT_BRANCH}`
10447
+ ]);
10448
+ const incomingLines = incomingResult.ok ? incomingResult.stdout.split("\n").filter((line) => line.length > 0) : [];
10449
+ if (incomingResult.ok && incomingLines.length === 0) {
10450
+ return null;
10451
+ }
10452
+ let behind = null;
10453
+ let ahead = null;
10454
+ const counts = await git([
10455
+ "rev-list",
10456
+ "--left-right",
10457
+ "--count",
10458
+ `origin/${DEFAULT_BRANCH}...HEAD`
10459
+ ]);
10460
+ if (counts.ok) {
10461
+ const [left, right] = counts.stdout.split(/\s+/);
10462
+ behind = Number(left);
10463
+ ahead = Number(right);
10464
+ if (!Number.isFinite(behind) || behind <= 0) {
10465
+ return null;
10466
+ }
10467
+ if (!Number.isFinite(ahead)) {
10468
+ ahead = null;
10469
+ }
10470
+ } else if (!incomingResult.ok) {
10471
+ const contained = await git([
10472
+ "merge-base",
10473
+ "--is-ancestor",
10474
+ `origin/${DEFAULT_BRANCH}`,
10475
+ "HEAD"
10476
+ ]);
10477
+ if (contained.ok) {
10478
+ return null;
10479
+ }
10480
+ log17.info(
10481
+ `behind by an unknown amount: rev-list and log both failed (${counts.error})`
10482
+ );
10483
+ }
10484
+ const status = await git(["status", "--porcelain"]);
10485
+ if (!status.ok) {
10486
+ log17.info(`could not read the working tree state: ${status.error}`);
10487
+ }
10488
+ return {
10489
+ upstream,
10490
+ behind,
10491
+ ahead,
10492
+ dirty: status.ok ? status.stdout.length > 0 : "unknown",
10493
+ incoming: incomingLines.slice(0, MAX_INCOMING),
10494
+ incomingTruncated: incomingLines.length > MAX_INCOMING
10495
+ };
10496
+ }
10497
+
10313
10498
  // src/headless/messageQueue.ts
10314
10499
  function holdRestoredUserItems(items) {
10315
10500
  return items.map(
@@ -10516,7 +10701,7 @@ var MessageQueue = class {
10516
10701
  };
10517
10702
 
10518
10703
  // src/headless/index.ts
10519
- var log17 = createLogger("headless");
10704
+ var log18 = createLogger("headless");
10520
10705
  var EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
10521
10706
  var LONG_RUNNING_TOOLS = /* @__PURE__ */ new Set(["runMethod", "testJewel"]);
10522
10707
  var LONG_RUNNING_TOOL_TIMEOUT_MS = 18e5;
@@ -10580,6 +10765,15 @@ var HeadlessSession = class {
10580
10765
  * Persisted to .remy-stats.json alongside the queue.
10581
10766
  */
10582
10767
  passivePen = [];
10768
+ /**
10769
+ * The workspace-behind note, and the upstream tip it was raised for.
10770
+ *
10771
+ * Rides the same sweep as the passive pen and for the same reason: a
10772
+ * workspace that is behind is worth knowing about before the next piece of
10773
+ * work, and worth nothing at all if nobody is working — so it must never
10774
+ * initiate a turn of its own.
10775
+ */
10776
+ workspaceNotice = emptyWorkspaceNotice();
10583
10777
  // External tool bridge
10584
10778
  pendingTools = /* @__PURE__ */ new Map();
10585
10779
  earlyResults = /* @__PURE__ */ new Map();
@@ -10626,6 +10820,8 @@ var HeadlessSession = class {
10626
10820
  }
10627
10821
  );
10628
10822
  this.passivePen = loadPassiveResults();
10823
+ this.workspaceNotice = loadWorkspaceNotice();
10824
+ void this.checkUpstream();
10629
10825
  this.persistStats();
10630
10826
  if (resumed) {
10631
10827
  this.emit("session_restored", {
@@ -10749,7 +10945,7 @@ var HeadlessSession = class {
10749
10945
  try {
10750
10946
  this.handleCancel("shutdown");
10751
10947
  } catch (err) {
10752
- log17.warn("Shutdown cancel failed", { error: err?.message });
10948
+ log18.warn("Shutdown cancel failed", { error: err?.message });
10753
10949
  }
10754
10950
  this.emit("stopping");
10755
10951
  this.emit("stopped");
@@ -10765,7 +10961,7 @@ var HeadlessSession = class {
10765
10961
  }
10766
10962
  const line = JSON.stringify(payload) + "\n";
10767
10963
  if (event === "history") {
10768
- log17.info("Wrote history event to stdout", {
10964
+ log18.info("Wrote history event to stdout", {
10769
10965
  requestId,
10770
10966
  bytes: line.length
10771
10967
  });
@@ -10812,9 +11008,45 @@ var HeadlessSession = class {
10812
11008
  this.sessionStats,
10813
11009
  this.queue.snapshot(),
10814
11010
  this.passivePen,
10815
- suggestCompactAt
11011
+ suggestCompactAt,
11012
+ this.workspaceNotice
10816
11013
  );
10817
11014
  }
11015
+ /**
11016
+ * Park a note if this workspace is missing work that is already in production.
11017
+ *
11018
+ * Once per upstream tip rather than once per boot. Remy restarts for reasons
11019
+ * that have nothing to do with the repo — a pod recycle, a crash, a new
11020
+ * session — and re-raising the same note on each of those turns a useful
11021
+ * signal into nagging. A colleague publishing again moves the tip, which
11022
+ * re-arms it; bringing the workspace current means there is nothing to raise.
11023
+ *
11024
+ * Never throws: this is called unawaited from the boot path, so an unhandled
11025
+ * rejection here would be a crash at the least recoverable moment.
11026
+ */
11027
+ async checkUpstream() {
11028
+ try {
11029
+ const status = await readUpstreamStatus();
11030
+ if (!status || status.upstream === this.workspaceNotice.lastNotedUpstream) {
11031
+ return;
11032
+ }
11033
+ this.workspaceNotice = {
11034
+ pendingNote: buildWorkspaceStatusMessage(status),
11035
+ lastNotedUpstream: status.upstream
11036
+ };
11037
+ this.persistStats();
11038
+ log18.info("workspace behind upstream; note parked for the next turn", {
11039
+ // The upstream sha is the dedupe key, so it is what makes "why did I
11040
+ // not get a note" answerable from the log alone.
11041
+ upstream: status.upstream,
11042
+ behind: status.behind,
11043
+ ahead: status.ahead,
11044
+ dirty: status.dirty
11045
+ });
11046
+ } catch (err) {
11047
+ log18.info(`upstream check failed: ${String(err)}`);
11048
+ }
11049
+ }
10818
11050
  //////////////////////////////////////////////////////////////////////////////
10819
11051
  // Background completions (tool-block mutation; message delivery via queue)
10820
11052
  //////////////////////////////////////////////////////////////////////////////
@@ -10861,7 +11093,7 @@ var HeadlessSession = class {
10861
11093
  if (this.sessionStats.lastContextSize <= threshold) {
10862
11094
  return;
10863
11095
  }
10864
- log17.info("Forced compaction gate triggered", {
11096
+ log18.info("Forced compaction gate triggered", {
10865
11097
  contextSize: this.sessionStats.lastContextSize,
10866
11098
  threshold,
10867
11099
  model: parentModel,
@@ -10881,7 +11113,7 @@ var HeadlessSession = class {
10881
11113
  onBackgroundComplete = (toolCallId, name, result, subAgentMessages) => {
10882
11114
  const notify = getToolByName(name)?.backgroundNotify ?? "wake";
10883
11115
  this.pendingBlockUpdates.push({ toolCallId, result, subAgentMessages });
10884
- log17.info("Background complete", {
11116
+ log18.info("Background complete", {
10885
11117
  toolCallId,
10886
11118
  name,
10887
11119
  notify,
@@ -11163,7 +11395,7 @@ var HeadlessSession = class {
11163
11395
  const { documents, images } = await persistAttachments(attachments);
11164
11396
  return buildUploadHeader(documents, images) || void 0;
11165
11397
  } catch (err) {
11166
- log17.warn("Attachment persistence failed", { error: err.message });
11398
+ log18.warn("Attachment persistence failed", { error: err.message });
11167
11399
  return void 0;
11168
11400
  }
11169
11401
  }
@@ -11223,7 +11455,7 @@ var HeadlessSession = class {
11223
11455
  }
11224
11456
  if (batch.length === 0) {
11225
11457
  if (landings > 0) {
11226
- log17.info("promptUser store landings passed through", { landings });
11458
+ log18.info("promptUser store landings passed through", { landings });
11227
11459
  }
11228
11460
  return raw;
11229
11461
  }
@@ -11231,7 +11463,7 @@ var HeadlessSession = class {
11231
11463
  try {
11232
11464
  results = await persistAttachmentList(batch);
11233
11465
  } catch (err) {
11234
- log17.warn("promptUser upload persistence failed", {
11466
+ log18.warn("promptUser upload persistence failed", {
11235
11467
  error: err.message
11236
11468
  });
11237
11469
  results = batch.map(() => null);
@@ -11243,7 +11475,7 @@ var HeadlessSession = class {
11243
11475
  return r.localPath;
11244
11476
  }
11245
11477
  const att = batch[cursor + i];
11246
- log17.warn("promptUser upload not persisted; falling back to url", {
11478
+ log18.warn("promptUser upload not persisted; falling back to url", {
11247
11479
  filename: att.filename
11248
11480
  });
11249
11481
  return att.url;
@@ -11251,7 +11483,7 @@ var HeadlessSession = class {
11251
11483
  cursor += slot.count;
11252
11484
  answers[slot.id] = slot.isArray ? paths : paths[0];
11253
11485
  }
11254
- log17.info("promptUser uploads persisted", { count: batch.length });
11486
+ log18.info("promptUser uploads persisted", { count: batch.length });
11255
11487
  return JSON.stringify(answers);
11256
11488
  }
11257
11489
  /**
@@ -11274,7 +11506,7 @@ var HeadlessSession = class {
11274
11506
  async runSingleTurn(parsed, requestId, fromChain = false, queued = false) {
11275
11507
  const attachments = parsed.attachments;
11276
11508
  if (attachments?.length) {
11277
- log17.info("Message has attachments", {
11509
+ log18.info("Message has attachments", {
11278
11510
  count: attachments.length,
11279
11511
  urls: attachments.map((a) => a.url)
11280
11512
  });
@@ -11422,6 +11654,18 @@ var HeadlessSession = class {
11422
11654
  hidden: true
11423
11655
  });
11424
11656
  }
11657
+ const hasUserWords = entries.some(
11658
+ (entry) => !entry.hidden && !isAutomatedMessage(entry.text)
11659
+ );
11660
+ if (this.workspaceNotice.pendingNote && hasUserWords) {
11661
+ const note = this.workspaceNotice.pendingNote;
11662
+ this.workspaceNotice = {
11663
+ ...this.workspaceNotice,
11664
+ pendingNote: null
11665
+ };
11666
+ this.persistStats();
11667
+ entries.unshift({ text: note, hidden: true });
11668
+ }
11425
11669
  const consumedRids = [];
11426
11670
  const takeSteering = async () => {
11427
11671
  const items = this.queue.removeWhere(
@@ -11474,7 +11718,7 @@ var HeadlessSession = class {
11474
11718
  error: "Turn ended unexpectedly"
11475
11719
  });
11476
11720
  }
11477
- log17.info("Turn complete", {
11721
+ log18.info("Turn complete", {
11478
11722
  requestId,
11479
11723
  durationMs: Date.now() - this.turnStart
11480
11724
  });
@@ -11486,7 +11730,7 @@ var HeadlessSession = class {
11486
11730
  error: err.message
11487
11731
  });
11488
11732
  }
11489
- log17.warn("Command failed", {
11733
+ log18.warn("Command failed", {
11490
11734
  action: "message",
11491
11735
  requestId,
11492
11736
  error: err.message
@@ -11793,7 +12037,7 @@ var HeadlessSession = class {
11793
12037
  try {
11794
12038
  parsed = JSON.parse(line);
11795
12039
  } catch (err) {
11796
- log17.warn("Invalid JSON on stdin", {
12040
+ log18.warn("Invalid JSON on stdin", {
11797
12041
  error: err.message,
11798
12042
  lineLength: line.length,
11799
12043
  preview: line.slice(0, 200)
@@ -11802,7 +12046,7 @@ var HeadlessSession = class {
11802
12046
  return;
11803
12047
  }
11804
12048
  const { action, requestId } = parsed;
11805
- log17.info("Command received", { action, requestId });
12049
+ log18.info("Command received", { action, requestId });
11806
12050
  if (action === "tool_result" && parsed.id) {
11807
12051
  const id = parsed.id;
11808
12052
  const result = parsed.result ?? "";
@@ -11811,7 +12055,7 @@ var HeadlessSession = class {
11811
12055
  this.pendingTools.delete(id);
11812
12056
  pending2.resolve(result);
11813
12057
  } else if (!this.running) {
11814
- log17.info("Late tool_result while idle, dismissing", { id });
12058
+ log18.info("Late tool_result while idle, dismissing", { id });
11815
12059
  this.emit("completed", { success: true }, requestId);
11816
12060
  } else {
11817
12061
  this.earlyResults.set(id, result);
@@ -11824,7 +12068,7 @@ var HeadlessSession = class {
11824
12068
  ...typeof parsed.before === "number" ? { before: parsed.before } : {},
11825
12069
  ...typeof parsed.limit === "number" ? { limit: parsed.limit } : {}
11826
12070
  });
11827
- log17.info("History response", {
12071
+ log18.info("History response", {
11828
12072
  requestId,
11829
12073
  startIndex: page.startIndex,
11830
12074
  endIndex: page.endIndex,