@mindstudio-ai/remy 0.1.331 → 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,
@@ -1141,6 +1147,33 @@ ${xml}
1141
1147
  </background_results>`;
1142
1148
  return automatedMessage("background_results", body);
1143
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
+ }
1144
1177
  function mergeBackgroundResultsMessages(messages) {
1145
1178
  const results = [];
1146
1179
  const toolRe = /<tool_result id="([^"]+)" name="([^"]+)">\n([\s\S]*?)\n<\/tool_result>/g;
@@ -3760,6 +3793,7 @@ var INTERNAL_PAYLOAD_MARKERS = [
3760
3793
  "[INTERRUPTED]",
3761
3794
  "[INTERRUPTED - PARTIAL OUTPUT RETRIEVED]",
3762
3795
  "<background_results>",
3796
+ "<workspace_status>",
3763
3797
  "<tool_result"
3764
3798
  ];
3765
3799
  function sanitizeStatusText(text) {
@@ -3814,7 +3848,11 @@ function startStatusWatcher(config) {
3814
3848
  method: "POST",
3815
3849
  headers: {
3816
3850
  "Content-Type": "application/json",
3817
- 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()
3818
3856
  },
3819
3857
  body: JSON.stringify({ appId: apiConfig.appId, context }),
3820
3858
  signal
@@ -4831,7 +4869,7 @@ TypeScript running in a sandboxed environment. Any npm package can be installed.
4831
4869
  - Managed SQLite database with typed schemas and automatic migrations. Define a TypeScript interface, push, and the platform handles diffing and migrating.
4832
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.
4833
4871
  - Encrypted secrets with separate dev/prod values, injected as process.env. For third-party service credentials not covered by the SDK.
4834
- - 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.
4835
4873
 
4836
4874
  ## MindStudio SDK
4837
4875
 
@@ -7867,6 +7905,9 @@ function serializeForSummary(messages) {
7867
7905
  }
7868
7906
  continue;
7869
7907
  }
7908
+ if (msg.role === "user" && (msg.hidden || typeof msg.content === "string" && isAutomatedMessage(msg.content))) {
7909
+ continue;
7910
+ }
7870
7911
  if (typeof msg.content === "string") {
7871
7912
  if (msg.content.trim()) {
7872
7913
  lines.push(`[${msg.role}]: ${msg.content}`);
@@ -9026,7 +9067,10 @@ function parsePartialJson(jsonString) {
9026
9067
  }
9027
9068
 
9028
9069
  // src/automatedActions/resolve.ts
9029
- var NON_ACTION_SENTINELS = /* @__PURE__ */ new Set(["background_results"]);
9070
+ var NON_ACTION_SENTINELS = /* @__PURE__ */ new Set([
9071
+ "background_results",
9072
+ "workspace_status"
9073
+ ]);
9030
9074
  function resolveAction(text) {
9031
9075
  const parsed = parseSentinel(text);
9032
9076
  if (!parsed) {
@@ -9402,7 +9446,9 @@ async function runTurn(params) {
9402
9446
  for (const entry of keptEntries) {
9403
9447
  appendEntry(entry);
9404
9448
  }
9405
- 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;
9406
9452
  const STATUS_EXCLUDED_TOOLS = /* @__PURE__ */ new Set([
9407
9453
  "markBuildComplete",
9408
9454
  "setProjectMetadata",
@@ -10296,7 +10342,24 @@ function loadPassiveResults() {
10296
10342
  }
10297
10343
  return [];
10298
10344
  }
10299
- 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) {
10300
10363
  try {
10301
10364
  writeFileAtomicSync(
10302
10365
  STATS_FILE,
@@ -10304,13 +10367,134 @@ function writeStats(stats, queue, passiveResults, suggestCompactAt) {
10304
10367
  ...stats,
10305
10368
  suggestCompactAt,
10306
10369
  queue,
10307
- passiveResults
10370
+ passiveResults,
10371
+ workspaceNotice
10308
10372
  })
10309
10373
  );
10310
10374
  } catch {
10311
10375
  }
10312
10376
  }
10313
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
+
10314
10498
  // src/headless/messageQueue.ts
10315
10499
  function holdRestoredUserItems(items) {
10316
10500
  return items.map(
@@ -10517,7 +10701,7 @@ var MessageQueue = class {
10517
10701
  };
10518
10702
 
10519
10703
  // src/headless/index.ts
10520
- var log17 = createLogger("headless");
10704
+ var log18 = createLogger("headless");
10521
10705
  var EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
10522
10706
  var LONG_RUNNING_TOOLS = /* @__PURE__ */ new Set(["runMethod", "testJewel"]);
10523
10707
  var LONG_RUNNING_TOOL_TIMEOUT_MS = 18e5;
@@ -10581,6 +10765,15 @@ var HeadlessSession = class {
10581
10765
  * Persisted to .remy-stats.json alongside the queue.
10582
10766
  */
10583
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();
10584
10777
  // External tool bridge
10585
10778
  pendingTools = /* @__PURE__ */ new Map();
10586
10779
  earlyResults = /* @__PURE__ */ new Map();
@@ -10627,6 +10820,8 @@ var HeadlessSession = class {
10627
10820
  }
10628
10821
  );
10629
10822
  this.passivePen = loadPassiveResults();
10823
+ this.workspaceNotice = loadWorkspaceNotice();
10824
+ void this.checkUpstream();
10630
10825
  this.persistStats();
10631
10826
  if (resumed) {
10632
10827
  this.emit("session_restored", {
@@ -10750,7 +10945,7 @@ var HeadlessSession = class {
10750
10945
  try {
10751
10946
  this.handleCancel("shutdown");
10752
10947
  } catch (err) {
10753
- log17.warn("Shutdown cancel failed", { error: err?.message });
10948
+ log18.warn("Shutdown cancel failed", { error: err?.message });
10754
10949
  }
10755
10950
  this.emit("stopping");
10756
10951
  this.emit("stopped");
@@ -10766,7 +10961,7 @@ var HeadlessSession = class {
10766
10961
  }
10767
10962
  const line = JSON.stringify(payload) + "\n";
10768
10963
  if (event === "history") {
10769
- log17.info("Wrote history event to stdout", {
10964
+ log18.info("Wrote history event to stdout", {
10770
10965
  requestId,
10771
10966
  bytes: line.length
10772
10967
  });
@@ -10813,9 +11008,45 @@ var HeadlessSession = class {
10813
11008
  this.sessionStats,
10814
11009
  this.queue.snapshot(),
10815
11010
  this.passivePen,
10816
- suggestCompactAt
11011
+ suggestCompactAt,
11012
+ this.workspaceNotice
10817
11013
  );
10818
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
+ }
10819
11050
  //////////////////////////////////////////////////////////////////////////////
10820
11051
  // Background completions (tool-block mutation; message delivery via queue)
10821
11052
  //////////////////////////////////////////////////////////////////////////////
@@ -10862,7 +11093,7 @@ var HeadlessSession = class {
10862
11093
  if (this.sessionStats.lastContextSize <= threshold) {
10863
11094
  return;
10864
11095
  }
10865
- log17.info("Forced compaction gate triggered", {
11096
+ log18.info("Forced compaction gate triggered", {
10866
11097
  contextSize: this.sessionStats.lastContextSize,
10867
11098
  threshold,
10868
11099
  model: parentModel,
@@ -10882,7 +11113,7 @@ var HeadlessSession = class {
10882
11113
  onBackgroundComplete = (toolCallId, name, result, subAgentMessages) => {
10883
11114
  const notify = getToolByName(name)?.backgroundNotify ?? "wake";
10884
11115
  this.pendingBlockUpdates.push({ toolCallId, result, subAgentMessages });
10885
- log17.info("Background complete", {
11116
+ log18.info("Background complete", {
10886
11117
  toolCallId,
10887
11118
  name,
10888
11119
  notify,
@@ -11164,7 +11395,7 @@ var HeadlessSession = class {
11164
11395
  const { documents, images } = await persistAttachments(attachments);
11165
11396
  return buildUploadHeader(documents, images) || void 0;
11166
11397
  } catch (err) {
11167
- log17.warn("Attachment persistence failed", { error: err.message });
11398
+ log18.warn("Attachment persistence failed", { error: err.message });
11168
11399
  return void 0;
11169
11400
  }
11170
11401
  }
@@ -11224,7 +11455,7 @@ var HeadlessSession = class {
11224
11455
  }
11225
11456
  if (batch.length === 0) {
11226
11457
  if (landings > 0) {
11227
- log17.info("promptUser store landings passed through", { landings });
11458
+ log18.info("promptUser store landings passed through", { landings });
11228
11459
  }
11229
11460
  return raw;
11230
11461
  }
@@ -11232,7 +11463,7 @@ var HeadlessSession = class {
11232
11463
  try {
11233
11464
  results = await persistAttachmentList(batch);
11234
11465
  } catch (err) {
11235
- log17.warn("promptUser upload persistence failed", {
11466
+ log18.warn("promptUser upload persistence failed", {
11236
11467
  error: err.message
11237
11468
  });
11238
11469
  results = batch.map(() => null);
@@ -11244,7 +11475,7 @@ var HeadlessSession = class {
11244
11475
  return r.localPath;
11245
11476
  }
11246
11477
  const att = batch[cursor + i];
11247
- log17.warn("promptUser upload not persisted; falling back to url", {
11478
+ log18.warn("promptUser upload not persisted; falling back to url", {
11248
11479
  filename: att.filename
11249
11480
  });
11250
11481
  return att.url;
@@ -11252,7 +11483,7 @@ var HeadlessSession = class {
11252
11483
  cursor += slot.count;
11253
11484
  answers[slot.id] = slot.isArray ? paths : paths[0];
11254
11485
  }
11255
- log17.info("promptUser uploads persisted", { count: batch.length });
11486
+ log18.info("promptUser uploads persisted", { count: batch.length });
11256
11487
  return JSON.stringify(answers);
11257
11488
  }
11258
11489
  /**
@@ -11275,7 +11506,7 @@ var HeadlessSession = class {
11275
11506
  async runSingleTurn(parsed, requestId, fromChain = false, queued = false) {
11276
11507
  const attachments = parsed.attachments;
11277
11508
  if (attachments?.length) {
11278
- log17.info("Message has attachments", {
11509
+ log18.info("Message has attachments", {
11279
11510
  count: attachments.length,
11280
11511
  urls: attachments.map((a) => a.url)
11281
11512
  });
@@ -11423,6 +11654,18 @@ var HeadlessSession = class {
11423
11654
  hidden: true
11424
11655
  });
11425
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
+ }
11426
11669
  const consumedRids = [];
11427
11670
  const takeSteering = async () => {
11428
11671
  const items = this.queue.removeWhere(
@@ -11475,7 +11718,7 @@ var HeadlessSession = class {
11475
11718
  error: "Turn ended unexpectedly"
11476
11719
  });
11477
11720
  }
11478
- log17.info("Turn complete", {
11721
+ log18.info("Turn complete", {
11479
11722
  requestId,
11480
11723
  durationMs: Date.now() - this.turnStart
11481
11724
  });
@@ -11487,7 +11730,7 @@ var HeadlessSession = class {
11487
11730
  error: err.message
11488
11731
  });
11489
11732
  }
11490
- log17.warn("Command failed", {
11733
+ log18.warn("Command failed", {
11491
11734
  action: "message",
11492
11735
  requestId,
11493
11736
  error: err.message
@@ -11794,7 +12037,7 @@ var HeadlessSession = class {
11794
12037
  try {
11795
12038
  parsed = JSON.parse(line);
11796
12039
  } catch (err) {
11797
- log17.warn("Invalid JSON on stdin", {
12040
+ log18.warn("Invalid JSON on stdin", {
11798
12041
  error: err.message,
11799
12042
  lineLength: line.length,
11800
12043
  preview: line.slice(0, 200)
@@ -11803,7 +12046,7 @@ var HeadlessSession = class {
11803
12046
  return;
11804
12047
  }
11805
12048
  const { action, requestId } = parsed;
11806
- log17.info("Command received", { action, requestId });
12049
+ log18.info("Command received", { action, requestId });
11807
12050
  if (action === "tool_result" && parsed.id) {
11808
12051
  const id = parsed.id;
11809
12052
  const result = parsed.result ?? "";
@@ -11812,7 +12055,7 @@ var HeadlessSession = class {
11812
12055
  this.pendingTools.delete(id);
11813
12056
  pending2.resolve(result);
11814
12057
  } else if (!this.running) {
11815
- log17.info("Late tool_result while idle, dismissing", { id });
12058
+ log18.info("Late tool_result while idle, dismissing", { id });
11816
12059
  this.emit("completed", { success: true }, requestId);
11817
12060
  } else {
11818
12061
  this.earlyResults.set(id, result);
@@ -11825,7 +12068,7 @@ var HeadlessSession = class {
11825
12068
  ...typeof parsed.before === "number" ? { before: parsed.before } : {},
11826
12069
  ...typeof parsed.limit === "number" ? { limit: parsed.limit } : {}
11827
12070
  });
11828
- log17.info("History response", {
12071
+ log18.info("History response", {
11829
12072
  requestId,
11830
12073
  startIndex: page.startIndex,
11831
12074
  endIndex: page.endIndex,