@sideboard-ai/core 0.1.327 → 0.1.328

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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  isAskUserToolName
4
- } from "../chunk-ULWPSBAB.js";
4
+ } from "../chunk-NJV5YEKI.js";
5
5
  import {
6
6
  isClaudeAiConnectorMcp,
7
7
  mcpServerIdForPermission,
@@ -1215,16 +1215,6 @@ function isAskUserToolName(name) {
1215
1215
  const base = name.replace(/^mcp__sideboard__/i, "");
1216
1216
  return ASK_USER_TOOL_RE.test(name) || /^ask_user$/i.test(base);
1217
1217
  }
1218
- function lastTopLevelToolIsAskUser(parts) {
1219
- if (!parts?.length) return false;
1220
- for (let i = parts.length - 1; i >= 0; i--) {
1221
- const part = parts[i];
1222
- if (part.type === "tool" && !part.parentId) {
1223
- return isAskUserToolName(part.name);
1224
- }
1225
- }
1226
- return false;
1227
- }
1228
1218
  function questionsFromToolPart(part) {
1229
1219
  const fromInput = parsePlanQuestionsInput(part.input);
1230
1220
  if (fromInput.length) return fromInput;
@@ -1240,7 +1230,7 @@ function extractPendingPlanQuestions(parts) {
1240
1230
  if (!parts?.length) return null;
1241
1231
  for (let i = parts.length - 1; i >= 0; i--) {
1242
1232
  const p = parts[i];
1243
- if (p.type !== "tool" || !isAskUserToolName(p.name)) continue;
1233
+ if (p.type !== "tool" || p.parentId || !isAskUserToolName(p.name)) continue;
1244
1234
  const questions = questionsFromToolPart(p);
1245
1235
  if (!questions.length) continue;
1246
1236
  return {
@@ -1272,8 +1262,32 @@ function isIncompleteTaskState(state) {
1272
1262
  function needsCoordinatorAction(state) {
1273
1263
  return isIncompleteTaskState(state) || state === "input-required";
1274
1264
  }
1275
- function endedOnAskUser(parts) {
1276
- return lastTopLevelToolIsAskUser(parts);
1265
+ function turnWaitingOnAskUser(parts, dismissedQuestionSignature) {
1266
+ if (!parts?.length) return false;
1267
+ let ask = null;
1268
+ for (let i = parts.length - 1; i >= 0; i--) {
1269
+ const part = parts[i];
1270
+ if (part.type === "tool" && !part.parentId && isAskUserToolName(part.name)) {
1271
+ ask = part;
1272
+ break;
1273
+ }
1274
+ }
1275
+ if (!ask) return false;
1276
+ const dismissed = dismissedQuestionSignature?.trim();
1277
+ if (!dismissed) return true;
1278
+ const pending = extractPendingPlanQuestions([ask]);
1279
+ if (!pending?.signature) return false;
1280
+ return pending.signature !== dismissed;
1281
+ }
1282
+ function userRepliedAfterAgent(messages, lastAgent) {
1283
+ if (!messages || !lastAgent) return false;
1284
+ const index = messages.lastIndexOf(lastAgent);
1285
+ if (index < 0) return false;
1286
+ for (let i = index + 1; i < messages.length; i++) {
1287
+ const message = messages[i];
1288
+ if (message.role === "user" && message.origin !== "continue") return true;
1289
+ }
1290
+ return false;
1277
1291
  }
1278
1292
  function deriveTaskState(input) {
1279
1293
  if (input.stillRunning) {
@@ -1286,7 +1300,7 @@ function deriveTaskState(input) {
1286
1300
  case "stopped":
1287
1301
  return "canceled";
1288
1302
  default:
1289
- return endedOnAskUser(input.lastAgentParts) ? "input-required" : "completed";
1303
+ return !input.userAnswered && turnWaitingOnAskUser(input.lastAgentParts, input.dismissedQuestionSignature) ? "input-required" : "completed";
1290
1304
  }
1291
1305
  }
1292
1306
 
@@ -1682,7 +1696,7 @@ var PORT_RANGE_SIZE = 10;
1682
1696
  var RECLAIM_RETRY_DELAY_MS = 40;
1683
1697
  var RECLAIM_TRIES = 8;
1684
1698
  async function killReclaimable(ports, worktreePath, signal) {
1685
- const { killListenersOnPorts: killListenersOnPorts2, killStaleWorktreeListeners: killStaleWorktreeListeners2 } = await import("./conductor-D62Q2XJC.js");
1699
+ const { killListenersOnPorts: killListenersOnPorts2, killStaleWorktreeListeners: killStaleWorktreeListeners2 } = await import("./conductor-U5LGJZUJ.js");
1686
1700
  if (worktreePath) killStaleWorktreeListeners2(ports, worktreePath, signal);
1687
1701
  else killListenersOnPorts2(ports);
1688
1702
  }
@@ -2369,6 +2383,7 @@ export {
2369
2383
  extractPendingPlanQuestions,
2370
2384
  askBlockStillWaiting,
2371
2385
  needsCoordinatorAction,
2386
+ userRepliedAfterAgent,
2372
2387
  deriveTaskState,
2373
2388
  mcpWaitForTurnTimeoutMs,
2374
2389
  mcpWaitTaskHint,
@@ -63,9 +63,6 @@ function lastTopLevelToolIsAskUser(parts) {
63
63
  }
64
64
  return false;
65
65
  }
66
- function hasTopLevelTool(parts) {
67
- return Boolean(parts?.some((part) => part.type === "tool" && !part.parentId));
68
- }
69
66
  function questionsFromToolPart(part) {
70
67
  const fromInput = parsePlanQuestionsInput(part.input);
71
68
  if (fromInput.length) return fromInput;
@@ -81,7 +78,7 @@ function extractPendingPlanQuestions(parts) {
81
78
  if (!parts?.length) return null;
82
79
  for (let i = parts.length - 1; i >= 0; i--) {
83
80
  const p = parts[i];
84
- if (p.type !== "tool" || !isAskUserToolName(p.name)) continue;
81
+ if (p.type !== "tool" || p.parentId || !isAskUserToolName(p.name)) continue;
85
82
  const questions = questionsFromToolPart(p);
86
83
  if (!questions.length) continue;
87
84
  return {
@@ -121,18 +118,23 @@ function askBlockStillWaiting(block, messages) {
121
118
  }
122
119
  function latestPendingPlanQuestions(input) {
123
120
  if (input.userReplied) return null;
121
+ const dismissed = input.dismissedQuestionSignature?.trim();
122
+ const accept = (pending) => {
123
+ if (!pending) return null;
124
+ if (dismissed && pending.signature === dismissed) return null;
125
+ return pending;
126
+ };
124
127
  const fromBlock = () => pendingFromAskBlock(input.agentBlock, input.dismissedQuestionSignature);
125
- if (lastTopLevelToolIsAskUser(input.liveParts)) {
126
- return extractPendingPlanQuestions(input.liveParts) ?? fromBlock();
127
- }
128
+ const liveAsk = accept(extractPendingPlanQuestions(input.liveParts));
129
+ if (liveAsk) return liveAsk;
128
130
  const last = input.messages[input.messages.length - 1];
129
- if (last?.role === "agent" && lastTopLevelToolIsAskUser(last.parts)) {
130
- return extractPendingPlanQuestions(last.parts) ?? fromBlock();
131
+ if (last?.role === "agent") {
132
+ const fromTranscript = accept(extractPendingPlanQuestions(last.parts));
133
+ if (fromTranscript) return fromTranscript;
131
134
  }
132
- if (hasTopLevelTool(input.liveParts) || last?.role === "agent" && hasTopLevelTool(last.parts)) {
135
+ if ((input.status === "running" || input.status === "queued") && !input.agentBlock?.onScreen) {
133
136
  return null;
134
137
  }
135
- if (input.status === "running" || input.status === "queued") return null;
136
138
  const stamped = fromBlock();
137
139
  if (stamped && askBlockStillWaiting(input.agentBlock, input.messages)) {
138
140
  return stamped;
@@ -51,10 +51,11 @@ import {
51
51
  startDevServer,
52
52
  threadsSharingWorktree,
53
53
  turnWatchedDetachedJob,
54
+ userRepliedAfterAgent,
54
55
  worktreeDevProcessKey,
55
56
  worktreeRunProcessKey,
56
57
  worktreeSetupProcessKey
57
- } from "./chunk-IXWPCOLQ.js";
58
+ } from "./chunk-X3XTO45Y.js";
58
59
  import {
59
60
  applyAgentEvent,
60
61
  clipAgentEventForPaint,
@@ -187,7 +188,7 @@ import {
187
188
  isAskUserToolName,
188
189
  parsePlanQuestionsInput,
189
190
  planQuestionsSignature
190
- } from "./chunk-ULWPSBAB.js";
191
+ } from "./chunk-NJV5YEKI.js";
191
192
  import {
192
193
  mcpServerIdForPermission,
193
194
  mcpServerPermissionUpdate
@@ -2011,6 +2012,9 @@ function decisionBlockFromParts(parts, planMode, at = (/* @__PURE__ */ new Date(
2011
2012
  at
2012
2013
  );
2013
2014
  }
2015
+ function attentionOutranksAsk(existing) {
2016
+ return existing?.source === "reported" || existing?.source === "permission";
2017
+ }
2014
2018
  function withStampedQuestions(decision, existing) {
2015
2019
  if (decision.source !== "ask_user" || decision.questions?.length) return decision;
2016
2020
  if (existing?.source !== "ask_user" || !existing.questions?.length) return decision;
@@ -2024,12 +2028,37 @@ function agentBlockAfterTurn(input) {
2024
2028
  if (input.failed) return null;
2025
2029
  const decision = decisionBlockFromParts(input.parts, input.planMode);
2026
2030
  if (decision) {
2031
+ if (decision.source === "ask_user" && attentionOutranksAsk(input.existing)) {
2032
+ return input.existing ?? null;
2033
+ }
2027
2034
  const next = withStampedQuestions(decision, input.existing);
2028
2035
  if (isDismissedAskUserBlock(next, input.dismissedQuestionSignature)) {
2029
2036
  return input.existing?.source === "reported" ? input.existing : null;
2030
2037
  }
2031
2038
  return next;
2032
2039
  }
2040
+ const pending = extractPendingPlanQuestions(input.parts);
2041
+ if (pending && attentionOutranksAsk(input.existing)) {
2042
+ return input.existing ?? null;
2043
+ }
2044
+ if (pending) {
2045
+ const kept = withStampedQuestions(
2046
+ {
2047
+ ...makeAgentBlock(
2048
+ "ask_user",
2049
+ formatAskUserNotifyMessage(pending.questions),
2050
+ input.existing?.source === "ask_user" ? input.existing.at : void 0
2051
+ ),
2052
+ onScreen: true,
2053
+ questionSignature: pending.signature,
2054
+ questions: pending.questions
2055
+ },
2056
+ input.existing
2057
+ );
2058
+ if (!isDismissedAskUserBlock(kept, input.dismissedQuestionSignature)) return kept;
2059
+ } else if (input.existing?.source === "ask_user" && input.existing.onScreen && input.existing.questions?.length && !isDismissedAskUserBlock(input.existing, input.dismissedQuestionSignature)) {
2060
+ return input.existing;
2061
+ }
2033
2062
  if (input.existing && input.existing.source !== "reported" && lastTopLevelTool(input.parts)) {
2034
2063
  return null;
2035
2064
  }
@@ -2058,11 +2087,15 @@ function askBlocksEqual(a, b) {
2058
2087
  if (!a || !b) return false;
2059
2088
  return a.source === b.source && a.reason === b.reason && a.at === b.at && a.onScreen === b.onScreen && a.questionSignature === b.questionSignature;
2060
2089
  }
2061
- function askBlockAfterLiveParts(existing, parts) {
2090
+ function askBlockAfterLiveParts(existing, parts, dismissedQuestionSignature) {
2091
+ if (attentionOutranksAsk(existing)) return existing ?? null;
2092
+ if (isDismissedAskUserBlock(existing, dismissedQuestionSignature)) return null;
2093
+ const dismissed = dismissedQuestionSignature?.trim() ?? "";
2094
+ let pending = extractPendingPlanQuestions(parts);
2095
+ if (dismissed && pending?.signature === dismissed) pending = null;
2062
2096
  const last = lastTopLevelTool(parts);
2063
2097
  if (last?.type === "tool" && isAskUserToolName(last.name)) {
2064
2098
  if (existing?.source === "ask_user" && existing.onScreen) return existing;
2065
- const pending = extractPendingPlanQuestions(parts);
2066
2099
  const questions = pending?.questions ?? existing?.questions;
2067
2100
  if (!questions?.length) {
2068
2101
  return existing?.source === "ask_user" ? { ...existing, onScreen: true } : existing ?? null;
@@ -2075,13 +2108,32 @@ function askBlockAfterLiveParts(existing, parts) {
2075
2108
  questions
2076
2109
  };
2077
2110
  }
2111
+ if (pending) {
2112
+ if (existing?.source === "ask_user" && existing.onScreen && existing.questions?.length) {
2113
+ return existing;
2114
+ }
2115
+ const at = existing?.source === "ask_user" ? existing.at : void 0;
2116
+ return {
2117
+ ...makeAgentBlock("ask_user", formatAskUserNotifyMessage(pending.questions), at),
2118
+ onScreen: true,
2119
+ questionSignature: pending.signature,
2120
+ questions: pending.questions
2121
+ };
2122
+ }
2123
+ if (existing?.source === "ask_user" && existing.onScreen && existing.questions?.length) {
2124
+ return existing;
2125
+ }
2078
2126
  if (last && existing?.source === "ask_user") return null;
2079
2127
  return existing ?? null;
2080
2128
  }
2081
2129
  function syncAskUserBlockFromLive(threadId, parts) {
2082
2130
  const thread = readThread(threadId);
2083
2131
  if (!thread || thread.status === "archived") return;
2084
- const next = askBlockAfterLiveParts(thread.agentBlock, parts);
2132
+ const next = askBlockAfterLiveParts(
2133
+ thread.agentBlock,
2134
+ parts,
2135
+ thread.dismissedQuestionSignature
2136
+ );
2085
2137
  if (askBlocksEqual(thread.agentBlock, next)) return;
2086
2138
  updateThread(threadId, { agentBlock: next });
2087
2139
  }
@@ -7091,7 +7143,7 @@ function formatScheduledPrompt(name, prompt) {
7091
7143
  ${prompt}`;
7092
7144
  }
7093
7145
  async function defaultDeps() {
7094
- const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-EN3ZKD73.js");
7146
+ const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-YY2JUYKD.js");
7095
7147
  const orch = getOrchestrator2();
7096
7148
  return {
7097
7149
  findThread: (id) => findThreadByRef(id),
@@ -9311,7 +9363,9 @@ ${formatEnvReminder(projectNameForRepoPath(thread.repoPath || thread.worktreePat
9311
9363
  const taskState = deriveTaskState({
9312
9364
  status: thread.status,
9313
9365
  stillRunning,
9314
- lastAgentParts: lastAgent?.parts
9366
+ lastAgentParts: lastAgent?.parts,
9367
+ dismissedQuestionSignature: thread.dismissedQuestionSignature,
9368
+ userAnswered: userRepliedAfterAgent(thread.messages, lastAgent)
9315
9369
  });
9316
9370
  const block = thread.agentBlock?.reason?.trim() ? thread.agentBlock : null;
9317
9371
  return {
@@ -29,8 +29,10 @@ import {
29
29
  ensureWorktreeSideboardIgnored
30
30
  } from "./chunk-QKHEVSB5.js";
31
31
  import {
32
+ extractPendingPlanQuestions,
33
+ isAskUserToolName,
32
34
  lastTopLevelToolIsAskUser
33
- } from "./chunk-ULWPSBAB.js";
35
+ } from "./chunk-NJV5YEKI.js";
34
36
  import {
35
37
  clipToolResultForStore
36
38
  } from "./chunk-OPXEJJBM.js";
@@ -1228,6 +1230,33 @@ function needsCoordinatorAction(state) {
1228
1230
  function endedOnAskUser(parts) {
1229
1231
  return lastTopLevelToolIsAskUser(parts);
1230
1232
  }
1233
+ function turnWaitingOnAskUser(parts, dismissedQuestionSignature) {
1234
+ if (!parts?.length) return false;
1235
+ let ask = null;
1236
+ for (let i = parts.length - 1; i >= 0; i--) {
1237
+ const part = parts[i];
1238
+ if (part.type === "tool" && !part.parentId && isAskUserToolName(part.name)) {
1239
+ ask = part;
1240
+ break;
1241
+ }
1242
+ }
1243
+ if (!ask) return false;
1244
+ const dismissed = dismissedQuestionSignature?.trim();
1245
+ if (!dismissed) return true;
1246
+ const pending = extractPendingPlanQuestions([ask]);
1247
+ if (!pending?.signature) return false;
1248
+ return pending.signature !== dismissed;
1249
+ }
1250
+ function userRepliedAfterAgent(messages, lastAgent) {
1251
+ if (!messages || !lastAgent) return false;
1252
+ const index = messages.lastIndexOf(lastAgent);
1253
+ if (index < 0) return false;
1254
+ for (let i = index + 1; i < messages.length; i++) {
1255
+ const message = messages[i];
1256
+ if (message.role === "user" && message.origin !== "continue") return true;
1257
+ }
1258
+ return false;
1259
+ }
1231
1260
  function deriveTaskState(input) {
1232
1261
  if (input.stillRunning) {
1233
1262
  return input.status === "queued" ? "submitted" : "working";
@@ -1239,7 +1268,7 @@ function deriveTaskState(input) {
1239
1268
  case "stopped":
1240
1269
  return "canceled";
1241
1270
  default:
1242
- return endedOnAskUser(input.lastAgentParts) ? "input-required" : "completed";
1271
+ return !input.userAnswered && turnWaitingOnAskUser(input.lastAgentParts, input.dismissedQuestionSignature) ? "input-required" : "completed";
1243
1272
  }
1244
1273
  }
1245
1274
 
@@ -1635,7 +1664,7 @@ var PORT_RANGE_SIZE = 10;
1635
1664
  var RECLAIM_RETRY_DELAY_MS = 40;
1636
1665
  var RECLAIM_TRIES = 8;
1637
1666
  async function killReclaimable(ports, worktreePath, signal) {
1638
- const { killListenersOnPorts: killListenersOnPorts2, killStaleWorktreeListeners: killStaleWorktreeListeners2 } = await import("./conductor-CP7TW6TN.js");
1667
+ const { killListenersOnPorts: killListenersOnPorts2, killStaleWorktreeListeners: killStaleWorktreeListeners2 } = await import("./conductor-G6GH5ZMU.js");
1639
1668
  if (worktreePath) killStaleWorktreeListeners2(ports, worktreePath, signal);
1640
1669
  else killListenersOnPorts2(ports);
1641
1670
  }
@@ -2320,6 +2349,7 @@ export {
2320
2349
  isIncompleteTaskState,
2321
2350
  needsCoordinatorAction,
2322
2351
  endedOnAskUser,
2352
+ userRepliedAfterAgent,
2323
2353
  deriveTaskState,
2324
2354
  mcpWaitForTurnTimeoutMs,
2325
2355
  mcpWaitTaskHint,
@@ -49,10 +49,11 @@ import {
49
49
  startDevServer,
50
50
  threadsSharingWorktree,
51
51
  turnWatchedDetachedJob,
52
+ userRepliedAfterAgent,
52
53
  worktreeDevProcessKey,
53
54
  worktreeRunProcessKey,
54
55
  worktreeSetupProcessKey
55
- } from "./chunk-RZJ3QJZT.js";
56
+ } from "./chunk-EYDLXVED.js";
56
57
  import {
57
58
  applyAgentEvent,
58
59
  clipAgentEventForPaint,
@@ -1887,6 +1888,9 @@ function decisionBlockFromParts(parts, planMode, at = (/* @__PURE__ */ new Date(
1887
1888
  at
1888
1889
  );
1889
1890
  }
1891
+ function attentionOutranksAsk(existing) {
1892
+ return existing?.source === "reported" || existing?.source === "permission";
1893
+ }
1890
1894
  function withStampedQuestions(decision, existing) {
1891
1895
  if (decision.source !== "ask_user" || decision.questions?.length) return decision;
1892
1896
  if (existing?.source !== "ask_user" || !existing.questions?.length) return decision;
@@ -1900,12 +1904,37 @@ function agentBlockAfterTurn(input) {
1900
1904
  if (input.failed) return null;
1901
1905
  const decision = decisionBlockFromParts(input.parts, input.planMode);
1902
1906
  if (decision) {
1907
+ if (decision.source === "ask_user" && attentionOutranksAsk(input.existing)) {
1908
+ return input.existing ?? null;
1909
+ }
1903
1910
  const next = withStampedQuestions(decision, input.existing);
1904
1911
  if (isDismissedAskUserBlock(next, input.dismissedQuestionSignature)) {
1905
1912
  return input.existing?.source === "reported" ? input.existing : null;
1906
1913
  }
1907
1914
  return next;
1908
1915
  }
1916
+ const pending = extractPendingPlanQuestions(input.parts);
1917
+ if (pending && attentionOutranksAsk(input.existing)) {
1918
+ return input.existing ?? null;
1919
+ }
1920
+ if (pending) {
1921
+ const kept = withStampedQuestions(
1922
+ {
1923
+ ...makeAgentBlock(
1924
+ "ask_user",
1925
+ formatAskUserNotifyMessage(pending.questions),
1926
+ input.existing?.source === "ask_user" ? input.existing.at : void 0
1927
+ ),
1928
+ onScreen: true,
1929
+ questionSignature: pending.signature,
1930
+ questions: pending.questions
1931
+ },
1932
+ input.existing
1933
+ );
1934
+ if (!isDismissedAskUserBlock(kept, input.dismissedQuestionSignature)) return kept;
1935
+ } else if (input.existing?.source === "ask_user" && input.existing.onScreen && input.existing.questions?.length && !isDismissedAskUserBlock(input.existing, input.dismissedQuestionSignature)) {
1936
+ return input.existing;
1937
+ }
1909
1938
  if (input.existing && input.existing.source !== "reported" && lastTopLevelTool(input.parts)) {
1910
1939
  return null;
1911
1940
  }
@@ -1934,11 +1963,15 @@ function askBlocksEqual(a, b) {
1934
1963
  if (!a || !b) return false;
1935
1964
  return a.source === b.source && a.reason === b.reason && a.at === b.at && a.onScreen === b.onScreen && a.questionSignature === b.questionSignature;
1936
1965
  }
1937
- function askBlockAfterLiveParts(existing, parts) {
1966
+ function askBlockAfterLiveParts(existing, parts, dismissedQuestionSignature) {
1967
+ if (attentionOutranksAsk(existing)) return existing ?? null;
1968
+ if (isDismissedAskUserBlock(existing, dismissedQuestionSignature)) return null;
1969
+ const dismissed = dismissedQuestionSignature?.trim() ?? "";
1970
+ let pending = extractPendingPlanQuestions(parts);
1971
+ if (dismissed && pending?.signature === dismissed) pending = null;
1938
1972
  const last = lastTopLevelTool(parts);
1939
1973
  if (last?.type === "tool" && isAskUserToolName(last.name)) {
1940
1974
  if (existing?.source === "ask_user" && existing.onScreen) return existing;
1941
- const pending = extractPendingPlanQuestions(parts);
1942
1975
  const questions = pending?.questions ?? existing?.questions;
1943
1976
  if (!questions?.length) {
1944
1977
  return existing?.source === "ask_user" ? { ...existing, onScreen: true } : existing ?? null;
@@ -1951,13 +1984,32 @@ function askBlockAfterLiveParts(existing, parts) {
1951
1984
  questions
1952
1985
  };
1953
1986
  }
1987
+ if (pending) {
1988
+ if (existing?.source === "ask_user" && existing.onScreen && existing.questions?.length) {
1989
+ return existing;
1990
+ }
1991
+ const at = existing?.source === "ask_user" ? existing.at : void 0;
1992
+ return {
1993
+ ...makeAgentBlock("ask_user", formatAskUserNotifyMessage(pending.questions), at),
1994
+ onScreen: true,
1995
+ questionSignature: pending.signature,
1996
+ questions: pending.questions
1997
+ };
1998
+ }
1999
+ if (existing?.source === "ask_user" && existing.onScreen && existing.questions?.length) {
2000
+ return existing;
2001
+ }
1954
2002
  if (last && existing?.source === "ask_user") return null;
1955
2003
  return existing ?? null;
1956
2004
  }
1957
2005
  function syncAskUserBlockFromLive(threadId, parts) {
1958
2006
  const thread = readThread(threadId);
1959
2007
  if (!thread || thread.status === "archived") return;
1960
- const next = askBlockAfterLiveParts(thread.agentBlock, parts);
2008
+ const next = askBlockAfterLiveParts(
2009
+ thread.agentBlock,
2010
+ parts,
2011
+ thread.dismissedQuestionSignature
2012
+ );
1961
2013
  if (askBlocksEqual(thread.agentBlock, next)) return;
1962
2014
  updateThread(threadId, { agentBlock: next });
1963
2015
  }
@@ -6741,7 +6793,7 @@ function formatScheduledPrompt(name, prompt) {
6741
6793
  ${prompt}`;
6742
6794
  }
6743
6795
  async function defaultDeps() {
6744
- const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-W4TZBBDO.js");
6796
+ const { getOrchestrator: getOrchestrator2, startOrchestration: startOrchestration2 } = await import("./orchestrator-3F53AZEN.js");
6745
6797
  const orch = getOrchestrator2();
6746
6798
  return {
6747
6799
  findThread: (id) => findThreadByRef(id),
@@ -8961,7 +9013,9 @@ ${formatEnvReminder(projectNameForRepoPath(thread.repoPath || thread.worktreePat
8961
9013
  const taskState = deriveTaskState({
8962
9014
  status: thread.status,
8963
9015
  stillRunning,
8964
- lastAgentParts: lastAgent?.parts
9016
+ lastAgentParts: lastAgent?.parts,
9017
+ dismissedQuestionSignature: thread.dismissedQuestionSignature,
9018
+ userAnswered: userRepliedAfterAgent(thread.messages, lastAgent)
8965
9019
  });
8966
9020
  const block = thread.agentBlock?.reason?.trim() ? thread.agentBlock : null;
8967
9021
  return {
@@ -32,13 +32,13 @@ import {
32
32
  tryReservePort,
33
33
  tryReservePorts,
34
34
  worktreeRunPortsPath
35
- } from "./chunk-IXWPCOLQ.js";
35
+ } from "./chunk-X3XTO45Y.js";
36
36
  import "./chunk-OOXRMTU6.js";
37
37
  import "./chunk-RDXODLEN.js";
38
38
  import "./chunk-R2C45NHT.js";
39
39
  import "./chunk-Y6OHM672.js";
40
40
  import "./chunk-QKHEVSB5.js";
41
- import "./chunk-ULWPSBAB.js";
41
+ import "./chunk-NJV5YEKI.js";
42
42
  import "./chunk-N6ZF3XRP.js";
43
43
  import "./chunk-OPXEJJBM.js";
44
44
  import "./chunk-6K64FJDD.js";
@@ -34,7 +34,7 @@ import {
34
34
  tryReservePort,
35
35
  tryReservePorts,
36
36
  worktreeRunPortsPath
37
- } from "./chunk-RZJ3QJZT.js";
37
+ } from "./chunk-EYDLXVED.js";
38
38
  import "./chunk-5MXLDDYF.js";
39
39
  import "./chunk-Q4UPRQOS.js";
40
40
  import "./chunk-JKHCADKB.js";
package/dist/index.cjs CHANGED
@@ -7383,9 +7383,6 @@ function lastTopLevelToolIsAskUser(parts) {
7383
7383
  }
7384
7384
  return false;
7385
7385
  }
7386
- function hasTopLevelTool(parts) {
7387
- return Boolean(parts?.some((part) => part.type === "tool" && !part.parentId));
7388
- }
7389
7386
  function questionsFromToolPart(part) {
7390
7387
  const fromInput = parsePlanQuestionsInput(part.input);
7391
7388
  if (fromInput.length) return fromInput;
@@ -7401,7 +7398,7 @@ function extractPendingPlanQuestions(parts) {
7401
7398
  if (!parts?.length) return null;
7402
7399
  for (let i = parts.length - 1; i >= 0; i--) {
7403
7400
  const p = parts[i];
7404
- if (p.type !== "tool" || !isAskUserToolName(p.name)) continue;
7401
+ if (p.type !== "tool" || p.parentId || !isAskUserToolName(p.name)) continue;
7405
7402
  const questions2 = questionsFromToolPart(p);
7406
7403
  if (!questions2.length) continue;
7407
7404
  return {
@@ -7441,18 +7438,23 @@ function askBlockStillWaiting(block, messages) {
7441
7438
  }
7442
7439
  function latestPendingPlanQuestions(input) {
7443
7440
  if (input.userReplied) return null;
7441
+ const dismissed = input.dismissedQuestionSignature?.trim();
7442
+ const accept = (pending) => {
7443
+ if (!pending) return null;
7444
+ if (dismissed && pending.signature === dismissed) return null;
7445
+ return pending;
7446
+ };
7444
7447
  const fromBlock = () => pendingFromAskBlock(input.agentBlock, input.dismissedQuestionSignature);
7445
- if (lastTopLevelToolIsAskUser(input.liveParts)) {
7446
- return extractPendingPlanQuestions(input.liveParts) ?? fromBlock();
7447
- }
7448
+ const liveAsk = accept(extractPendingPlanQuestions(input.liveParts));
7449
+ if (liveAsk) return liveAsk;
7448
7450
  const last = input.messages[input.messages.length - 1];
7449
- if (last?.role === "agent" && lastTopLevelToolIsAskUser(last.parts)) {
7450
- return extractPendingPlanQuestions(last.parts) ?? fromBlock();
7451
+ if (last?.role === "agent") {
7452
+ const fromTranscript = accept(extractPendingPlanQuestions(last.parts));
7453
+ if (fromTranscript) return fromTranscript;
7451
7454
  }
7452
- if (hasTopLevelTool(input.liveParts) || last?.role === "agent" && hasTopLevelTool(last.parts)) {
7455
+ if ((input.status === "running" || input.status === "queued") && !input.agentBlock?.onScreen) {
7453
7456
  return null;
7454
7457
  }
7455
- if (input.status === "running" || input.status === "queued") return null;
7456
7458
  const stamped = fromBlock();
7457
7459
  if (stamped && askBlockStillWaiting(input.agentBlock, input.messages)) {
7458
7460
  return stamped;
@@ -9174,6 +9176,33 @@ function needsCoordinatorAction(state) {
9174
9176
  function endedOnAskUser(parts) {
9175
9177
  return lastTopLevelToolIsAskUser(parts);
9176
9178
  }
9179
+ function turnWaitingOnAskUser(parts, dismissedQuestionSignature) {
9180
+ if (!parts?.length) return false;
9181
+ let ask = null;
9182
+ for (let i = parts.length - 1; i >= 0; i--) {
9183
+ const part = parts[i];
9184
+ if (part.type === "tool" && !part.parentId && isAskUserToolName(part.name)) {
9185
+ ask = part;
9186
+ break;
9187
+ }
9188
+ }
9189
+ if (!ask) return false;
9190
+ const dismissed = dismissedQuestionSignature?.trim();
9191
+ if (!dismissed) return true;
9192
+ const pending = extractPendingPlanQuestions([ask]);
9193
+ if (!pending?.signature) return false;
9194
+ return pending.signature !== dismissed;
9195
+ }
9196
+ function userRepliedAfterAgent(messages, lastAgent) {
9197
+ if (!messages || !lastAgent) return false;
9198
+ const index = messages.lastIndexOf(lastAgent);
9199
+ if (index < 0) return false;
9200
+ for (let i = index + 1; i < messages.length; i++) {
9201
+ const message = messages[i];
9202
+ if (message.role === "user" && message.origin !== "continue") return true;
9203
+ }
9204
+ return false;
9205
+ }
9177
9206
  function deriveTaskState(input) {
9178
9207
  if (input.stillRunning) {
9179
9208
  return input.status === "queued" ? "submitted" : "working";
@@ -9185,7 +9214,7 @@ function deriveTaskState(input) {
9185
9214
  case "stopped":
9186
9215
  return "canceled";
9187
9216
  default:
9188
- return endedOnAskUser(input.lastAgentParts) ? "input-required" : "completed";
9217
+ return !input.userAnswered && turnWaitingOnAskUser(input.lastAgentParts, input.dismissedQuestionSignature) ? "input-required" : "completed";
9189
9218
  }
9190
9219
  }
9191
9220
  var init_task_state = __esm({
@@ -25625,6 +25654,9 @@ function decisionBlockFromParts(parts, planMode, at = (/* @__PURE__ */ new Date(
25625
25654
  at
25626
25655
  );
25627
25656
  }
25657
+ function attentionOutranksAsk(existing) {
25658
+ return existing?.source === "reported" || existing?.source === "permission";
25659
+ }
25628
25660
  function withStampedQuestions(decision, existing) {
25629
25661
  if (decision.source !== "ask_user" || decision.questions?.length) return decision;
25630
25662
  if (existing?.source !== "ask_user" || !existing.questions?.length) return decision;
@@ -25638,12 +25670,37 @@ function agentBlockAfterTurn(input) {
25638
25670
  if (input.failed) return null;
25639
25671
  const decision = decisionBlockFromParts(input.parts, input.planMode);
25640
25672
  if (decision) {
25673
+ if (decision.source === "ask_user" && attentionOutranksAsk(input.existing)) {
25674
+ return input.existing ?? null;
25675
+ }
25641
25676
  const next = withStampedQuestions(decision, input.existing);
25642
25677
  if (isDismissedAskUserBlock(next, input.dismissedQuestionSignature)) {
25643
25678
  return input.existing?.source === "reported" ? input.existing : null;
25644
25679
  }
25645
25680
  return next;
25646
25681
  }
25682
+ const pending = extractPendingPlanQuestions(input.parts);
25683
+ if (pending && attentionOutranksAsk(input.existing)) {
25684
+ return input.existing ?? null;
25685
+ }
25686
+ if (pending) {
25687
+ const kept = withStampedQuestions(
25688
+ {
25689
+ ...makeAgentBlock(
25690
+ "ask_user",
25691
+ formatAskUserNotifyMessage(pending.questions),
25692
+ input.existing?.source === "ask_user" ? input.existing.at : void 0
25693
+ ),
25694
+ onScreen: true,
25695
+ questionSignature: pending.signature,
25696
+ questions: pending.questions
25697
+ },
25698
+ input.existing
25699
+ );
25700
+ if (!isDismissedAskUserBlock(kept, input.dismissedQuestionSignature)) return kept;
25701
+ } else if (input.existing?.source === "ask_user" && input.existing.onScreen && input.existing.questions?.length && !isDismissedAskUserBlock(input.existing, input.dismissedQuestionSignature)) {
25702
+ return input.existing;
25703
+ }
25647
25704
  if (input.existing && input.existing.source !== "reported" && lastTopLevelTool(input.parts)) {
25648
25705
  return null;
25649
25706
  }
@@ -25672,11 +25729,15 @@ function askBlocksEqual(a, b) {
25672
25729
  if (!a || !b) return false;
25673
25730
  return a.source === b.source && a.reason === b.reason && a.at === b.at && a.onScreen === b.onScreen && a.questionSignature === b.questionSignature;
25674
25731
  }
25675
- function askBlockAfterLiveParts(existing, parts) {
25732
+ function askBlockAfterLiveParts(existing, parts, dismissedQuestionSignature) {
25733
+ if (attentionOutranksAsk(existing)) return existing ?? null;
25734
+ if (isDismissedAskUserBlock(existing, dismissedQuestionSignature)) return null;
25735
+ const dismissed = dismissedQuestionSignature?.trim() ?? "";
25736
+ let pending = extractPendingPlanQuestions(parts);
25737
+ if (dismissed && pending?.signature === dismissed) pending = null;
25676
25738
  const last = lastTopLevelTool(parts);
25677
25739
  if (last?.type === "tool" && isAskUserToolName(last.name)) {
25678
25740
  if (existing?.source === "ask_user" && existing.onScreen) return existing;
25679
- const pending = extractPendingPlanQuestions(parts);
25680
25741
  const questions2 = pending?.questions ?? existing?.questions;
25681
25742
  if (!questions2?.length) {
25682
25743
  return existing?.source === "ask_user" ? { ...existing, onScreen: true } : existing ?? null;
@@ -25689,13 +25750,32 @@ function askBlockAfterLiveParts(existing, parts) {
25689
25750
  questions: questions2
25690
25751
  };
25691
25752
  }
25753
+ if (pending) {
25754
+ if (existing?.source === "ask_user" && existing.onScreen && existing.questions?.length) {
25755
+ return existing;
25756
+ }
25757
+ const at = existing?.source === "ask_user" ? existing.at : void 0;
25758
+ return {
25759
+ ...makeAgentBlock("ask_user", formatAskUserNotifyMessage(pending.questions), at),
25760
+ onScreen: true,
25761
+ questionSignature: pending.signature,
25762
+ questions: pending.questions
25763
+ };
25764
+ }
25765
+ if (existing?.source === "ask_user" && existing.onScreen && existing.questions?.length) {
25766
+ return existing;
25767
+ }
25692
25768
  if (last && existing?.source === "ask_user") return null;
25693
25769
  return existing ?? null;
25694
25770
  }
25695
25771
  function syncAskUserBlockFromLive(threadId, parts) {
25696
25772
  const thread = readThread(threadId);
25697
25773
  if (!thread || thread.status === "archived") return;
25698
- const next = askBlockAfterLiveParts(thread.agentBlock, parts);
25774
+ const next = askBlockAfterLiveParts(
25775
+ thread.agentBlock,
25776
+ parts,
25777
+ thread.dismissedQuestionSignature
25778
+ );
25699
25779
  if (askBlocksEqual(thread.agentBlock, next)) return;
25700
25780
  updateThread(threadId, { agentBlock: next });
25701
25781
  }
@@ -34724,7 +34804,9 @@ ${formatEnvReminder(projectNameForRepoPath(thread.repoPath || thread.worktreePat
34724
34804
  const taskState2 = deriveTaskState({
34725
34805
  status: thread.status,
34726
34806
  stillRunning,
34727
- lastAgentParts: lastAgent?.parts
34807
+ lastAgentParts: lastAgent?.parts,
34808
+ dismissedQuestionSignature: thread.dismissedQuestionSignature,
34809
+ userAnswered: userRepliedAfterAgent(thread.messages, lastAgent)
34728
34810
  });
34729
34811
  const block = thread.agentBlock?.reason?.trim() ? thread.agentBlock : null;
34730
34812
  return {
package/dist/index.d.cts CHANGED
@@ -8,8 +8,9 @@ import { EventEmitter } from 'node:events';
8
8
  * Why an agent is waiting on a person. Process liveness stays on
9
9
  * `ThreadStatus`; this is the attention flag (Herdr `blocked`).
10
10
  * Cleared when the next user message starts a turn. An ask_user highlight
11
- * is shown only while that question is on screen in the agent, and drops
12
- * when the picker is gone (skip, a later user message, or the agent moved on).
11
+ * stays until the user answers or skips. A later tool in the same turn
12
+ * does not remove it — the tool returns immediately, and the model often
13
+ * keeps going before the person can answer.
13
14
  */
14
15
  type AgentBlockSource = 'ask_user' | 'plan' | 'reported' | 'permission';
15
16
  /** Composer Allow / Deny while a tool waits on the user. */
@@ -47,9 +48,9 @@ interface AgentBlock {
47
48
  }>;
48
49
  }>;
49
50
  /**
50
- * True once ask_user is the last top-level tool in the live stream or
51
- * the finished turn. A mid-turn stamp without this stays off the
52
- * sidebar / board / phone until the picker is actually on screen.
51
+ * True once ask_user is in the live stream or the finished turn.
52
+ * A mid-turn stamp without this stays off the sidebar / board / phone
53
+ * until the picker is actually on screen. A later tool does not clear it.
53
54
  */
54
55
  onScreen?: boolean;
55
56
  permission?: AgentPermissionPrompt;
@@ -5833,7 +5834,7 @@ declare function applyThreadIntoMain(thread: Pick<Thread, 'repoPath' | 'worktree
5833
5834
  * |------------------|-----------------------------------------------------------|
5834
5835
  * | submitted | queued — waiting for a concurrency slot, not started |
5835
5836
  * | working | the agent is running this turn |
5836
- * | input-required | turn ended on `ask_user`; the human must answer |
5837
+ * | input-required | unanswered `ask_user`, even if a later tool ran |
5837
5838
  * | completed | turn ended with an answer |
5838
5839
  * | failed | runner / agent error, or worktree missing (`broken`) |
5839
5840
  * | canceled | force-stopped before finishing |
@@ -5855,6 +5856,9 @@ declare function deriveTaskState(input: {
5855
5856
  stillRunning: boolean;
5856
5857
  /** Parts of the last agent reply (not an injected notice). */
5857
5858
  lastAgentParts?: MessagePart[];
5859
+ dismissedQuestionSignature?: string | null;
5860
+ /** A real user message landed after the agent reply that asked. */
5861
+ userAnswered?: boolean;
5858
5862
  }): TaskState;
5859
5863
  /** Snapshot returned by `getTurnResult`. Block fields are optional so older test doubles typecheck. */
5860
5864
  type TurnResult = {
@@ -6636,7 +6640,7 @@ type ToolPartLike = {
6636
6640
  };
6637
6641
  /** True when the last top-level tool (not a nested child) is ask_user. */
6638
6642
  declare function lastTopLevelToolIsAskUser(parts: ToolPartLike[] | undefined | null): boolean;
6639
- /** Newest ask-user tool part with parseable questions (live or persisted). */
6643
+ /** Newest top-level ask-user tool with parseable questions. Nested calls do not count. */
6640
6644
  declare function extractPendingPlanQuestions(parts: ToolPartLike[] | undefined | null): PendingPlanQuestions | null;
6641
6645
  /** Sidebar ask_user highlight, including questions stamped at call time. */
6642
6646
  interface AskBlockSnapshot {
@@ -6644,6 +6648,8 @@ interface AskBlockSnapshot {
6644
6648
  at?: string;
6645
6649
  questionSignature?: string;
6646
6650
  questions?: PlanQuestion[];
6651
+ /** Set once the picker is in the live stream. A mid-turn stamp omits it. */
6652
+ onScreen?: boolean;
6647
6653
  }
6648
6654
  /**
6649
6655
  * True when no later user reply has landed after this ask.
@@ -6675,7 +6681,10 @@ declare function latestPendingPlanQuestions(input: {
6675
6681
  */
6676
6682
  agentBlock?: AskBlockSnapshot | null;
6677
6683
  dismissedQuestionSignature?: string | null;
6678
- /** Hide a mid-turn stamp until the ask is the last tool (or the turn ends). */
6684
+ /**
6685
+ * Hide a mid-turn stamp until the ask is in the stream (or the turn
6686
+ * ends). Once the picker is on screen, a later tool must not hide it.
6687
+ */
6679
6688
  status?: string;
6680
6689
  }): PendingPlanQuestions | null;
6681
6690
  interface PlanQuestionAnswer {
@@ -6994,7 +7003,7 @@ declare function worktreeBoardStatus(group: Array<Pick<Thread, 'status'>>): Thre
6994
7003
  * Blocked highlight. Errors and archived chats keep their own status.
6995
7004
  * An ask_user stamp lands when the tool is called — before the picker is on
6996
7005
  * screen — so hide it while the turn is in flight unless `onScreen` (the
6997
- * live last tool is ask_user). Drop it once the chat is no longer asking.
7006
+ * ask is in the live stream). Drop it once the user answers or skips.
6998
7007
  */
6999
7008
  declare function visibleAgentBlock(thread: AgentBlockThread): AgentBlock | null;
7000
7009
  /**
package/dist/index.d.ts CHANGED
@@ -8,8 +8,9 @@ import { EventEmitter } from 'node:events';
8
8
  * Why an agent is waiting on a person. Process liveness stays on
9
9
  * `ThreadStatus`; this is the attention flag (Herdr `blocked`).
10
10
  * Cleared when the next user message starts a turn. An ask_user highlight
11
- * is shown only while that question is on screen in the agent, and drops
12
- * when the picker is gone (skip, a later user message, or the agent moved on).
11
+ * stays until the user answers or skips. A later tool in the same turn
12
+ * does not remove it — the tool returns immediately, and the model often
13
+ * keeps going before the person can answer.
13
14
  */
14
15
  type AgentBlockSource = 'ask_user' | 'plan' | 'reported' | 'permission';
15
16
  /** Composer Allow / Deny while a tool waits on the user. */
@@ -47,9 +48,9 @@ interface AgentBlock {
47
48
  }>;
48
49
  }>;
49
50
  /**
50
- * True once ask_user is the last top-level tool in the live stream or
51
- * the finished turn. A mid-turn stamp without this stays off the
52
- * sidebar / board / phone until the picker is actually on screen.
51
+ * True once ask_user is in the live stream or the finished turn.
52
+ * A mid-turn stamp without this stays off the sidebar / board / phone
53
+ * until the picker is actually on screen. A later tool does not clear it.
53
54
  */
54
55
  onScreen?: boolean;
55
56
  permission?: AgentPermissionPrompt;
@@ -5833,7 +5834,7 @@ declare function applyThreadIntoMain(thread: Pick<Thread, 'repoPath' | 'worktree
5833
5834
  * |------------------|-----------------------------------------------------------|
5834
5835
  * | submitted | queued — waiting for a concurrency slot, not started |
5835
5836
  * | working | the agent is running this turn |
5836
- * | input-required | turn ended on `ask_user`; the human must answer |
5837
+ * | input-required | unanswered `ask_user`, even if a later tool ran |
5837
5838
  * | completed | turn ended with an answer |
5838
5839
  * | failed | runner / agent error, or worktree missing (`broken`) |
5839
5840
  * | canceled | force-stopped before finishing |
@@ -5855,6 +5856,9 @@ declare function deriveTaskState(input: {
5855
5856
  stillRunning: boolean;
5856
5857
  /** Parts of the last agent reply (not an injected notice). */
5857
5858
  lastAgentParts?: MessagePart[];
5859
+ dismissedQuestionSignature?: string | null;
5860
+ /** A real user message landed after the agent reply that asked. */
5861
+ userAnswered?: boolean;
5858
5862
  }): TaskState;
5859
5863
  /** Snapshot returned by `getTurnResult`. Block fields are optional so older test doubles typecheck. */
5860
5864
  type TurnResult = {
@@ -6636,7 +6640,7 @@ type ToolPartLike = {
6636
6640
  };
6637
6641
  /** True when the last top-level tool (not a nested child) is ask_user. */
6638
6642
  declare function lastTopLevelToolIsAskUser(parts: ToolPartLike[] | undefined | null): boolean;
6639
- /** Newest ask-user tool part with parseable questions (live or persisted). */
6643
+ /** Newest top-level ask-user tool with parseable questions. Nested calls do not count. */
6640
6644
  declare function extractPendingPlanQuestions(parts: ToolPartLike[] | undefined | null): PendingPlanQuestions | null;
6641
6645
  /** Sidebar ask_user highlight, including questions stamped at call time. */
6642
6646
  interface AskBlockSnapshot {
@@ -6644,6 +6648,8 @@ interface AskBlockSnapshot {
6644
6648
  at?: string;
6645
6649
  questionSignature?: string;
6646
6650
  questions?: PlanQuestion[];
6651
+ /** Set once the picker is in the live stream. A mid-turn stamp omits it. */
6652
+ onScreen?: boolean;
6647
6653
  }
6648
6654
  /**
6649
6655
  * True when no later user reply has landed after this ask.
@@ -6675,7 +6681,10 @@ declare function latestPendingPlanQuestions(input: {
6675
6681
  */
6676
6682
  agentBlock?: AskBlockSnapshot | null;
6677
6683
  dismissedQuestionSignature?: string | null;
6678
- /** Hide a mid-turn stamp until the ask is the last tool (or the turn ends). */
6684
+ /**
6685
+ * Hide a mid-turn stamp until the ask is in the stream (or the turn
6686
+ * ends). Once the picker is on screen, a later tool must not hide it.
6687
+ */
6679
6688
  status?: string;
6680
6689
  }): PendingPlanQuestions | null;
6681
6690
  interface PlanQuestionAnswer {
@@ -6994,7 +7003,7 @@ declare function worktreeBoardStatus(group: Array<Pick<Thread, 'status'>>): Thre
6994
7003
  * Blocked highlight. Errors and archived chats keep their own status.
6995
7004
  * An ask_user stamp lands when the tool is called — before the picker is on
6996
7005
  * screen — so hide it while the turn is in flight unless `onScreen` (the
6997
- * live last tool is ask_user). Drop it once the chat is no longer asking.
7006
+ * ask is in the live stream). Drop it once the user answers or skips.
6998
7007
  */
6999
7008
  declare function visibleAgentBlock(thread: AgentBlockThread): AgentBlock | null;
7000
7009
  /**
package/dist/index.js CHANGED
@@ -218,7 +218,7 @@ import {
218
218
  worktreeCleanupSettings,
219
219
  writeAgentBlock,
220
220
  writeWorktreeFile
221
- } from "./chunk-DEB47BII.js";
221
+ } from "./chunk-WD222BVP.js";
222
222
  import {
223
223
  OPTIONAL_SERVICES,
224
224
  OPTIONAL_SERVICE_IDS,
@@ -512,7 +512,7 @@ import {
512
512
  workspaceAgentChatSummaries,
513
513
  worktreeRunPortsPath,
514
514
  wrapReviewSkillMarkdown
515
- } from "./chunk-IXWPCOLQ.js";
515
+ } from "./chunk-X3XTO45Y.js";
516
516
  import {
517
517
  STDERR_EVENT_MAX_CHARS,
518
518
  STDOUT_EVENT_MAX_CHARS,
@@ -833,7 +833,7 @@ import {
833
833
  latestPendingPlanQuestions,
834
834
  parsePlanQuestionsInput,
835
835
  planQuestionsSignature
836
- } from "./chunk-ULWPSBAB.js";
836
+ } from "./chunk-NJV5YEKI.js";
837
837
  import {
838
838
  BASE_ALLOWED_TOOLS,
839
839
  CLAUDE_CHROME_ALLOWED_TOOLS,
@@ -7685,7 +7685,7 @@ async function startHeadlessHost(opts) {
7685
7685
  };
7686
7686
  const refreshRunning = async () => {
7687
7687
  try {
7688
- const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-EN3ZKD73.js");
7688
+ const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-YY2JUYKD.js");
7689
7689
  agentsRunning = getOrchestrator2().getRuntime().running;
7690
7690
  } catch {
7691
7691
  agentsRunning = 0;
@@ -9828,7 +9828,7 @@ async function handleJobStop(params, _ctx) {
9828
9828
  return stopDetachedJob(jobCwd(rec), id, reason ? { reason } : void 0);
9829
9829
  }
9830
9830
  async function defaultTurnWait(ref, timeoutMs) {
9831
- const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-EN3ZKD73.js");
9831
+ const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-YY2JUYKD.js");
9832
9832
  const orch = getOrchestrator2();
9833
9833
  const thread = await orch.waitForTurn(ref, timeoutMs, {
9834
9834
  resolveIfStillRunning: true
@@ -13496,7 +13496,7 @@ async function continueSourceThread(threadId, prompt) {
13496
13496
  await continueOnReply(threadId, prompt);
13497
13497
  return;
13498
13498
  }
13499
- const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-EN3ZKD73.js");
13499
+ const { getOrchestrator: getOrchestrator2 } = await import("./orchestrator-YY2JUYKD.js");
13500
13500
  await getOrchestrator2().send(threadId, prompt);
13501
13501
  } catch {
13502
13502
  }
@@ -2153,16 +2153,6 @@ function isAskUserToolName(name) {
2153
2153
  const base = name.replace(/^mcp__sideboard__/i, "");
2154
2154
  return ASK_USER_TOOL_RE.test(name) || /^ask_user$/i.test(base);
2155
2155
  }
2156
- function lastTopLevelToolIsAskUser(parts) {
2157
- if (!parts?.length) return false;
2158
- for (let i = parts.length - 1; i >= 0; i--) {
2159
- const part = parts[i];
2160
- if (part.type === "tool" && !part.parentId) {
2161
- return isAskUserToolName(part.name);
2162
- }
2163
- }
2164
- return false;
2165
- }
2166
2156
  function questionsFromToolPart(part) {
2167
2157
  const fromInput = parsePlanQuestionsInput(part.input);
2168
2158
  if (fromInput.length) return fromInput;
@@ -2178,7 +2168,7 @@ function extractPendingPlanQuestions(parts) {
2178
2168
  if (!parts?.length) return null;
2179
2169
  for (let i = parts.length - 1; i >= 0; i--) {
2180
2170
  const p = parts[i];
2181
- if (p.type !== "tool" || !isAskUserToolName(p.name)) continue;
2171
+ if (p.type !== "tool" || p.parentId || !isAskUserToolName(p.name)) continue;
2182
2172
  const questions = questionsFromToolPart(p);
2183
2173
  if (!questions.length) continue;
2184
2174
  return {
@@ -2217,8 +2207,32 @@ function isIncompleteTaskState(state) {
2217
2207
  function needsCoordinatorAction(state) {
2218
2208
  return isIncompleteTaskState(state) || state === "input-required";
2219
2209
  }
2220
- function endedOnAskUser(parts) {
2221
- return lastTopLevelToolIsAskUser(parts);
2210
+ function turnWaitingOnAskUser(parts, dismissedQuestionSignature) {
2211
+ if (!parts?.length) return false;
2212
+ let ask = null;
2213
+ for (let i = parts.length - 1; i >= 0; i--) {
2214
+ const part = parts[i];
2215
+ if (part.type === "tool" && !part.parentId && isAskUserToolName(part.name)) {
2216
+ ask = part;
2217
+ break;
2218
+ }
2219
+ }
2220
+ if (!ask) return false;
2221
+ const dismissed = dismissedQuestionSignature?.trim();
2222
+ if (!dismissed) return true;
2223
+ const pending = extractPendingPlanQuestions([ask]);
2224
+ if (!pending?.signature) return false;
2225
+ return pending.signature !== dismissed;
2226
+ }
2227
+ function userRepliedAfterAgent(messages, lastAgent) {
2228
+ if (!messages || !lastAgent) return false;
2229
+ const index = messages.lastIndexOf(lastAgent);
2230
+ if (index < 0) return false;
2231
+ for (let i = index + 1; i < messages.length; i++) {
2232
+ const message = messages[i];
2233
+ if (message.role === "user" && message.origin !== "continue") return true;
2234
+ }
2235
+ return false;
2222
2236
  }
2223
2237
  function deriveTaskState(input) {
2224
2238
  if (input.stillRunning) {
@@ -2231,7 +2245,7 @@ function deriveTaskState(input) {
2231
2245
  case "stopped":
2232
2246
  return "canceled";
2233
2247
  default:
2234
- return endedOnAskUser(input.lastAgentParts) ? "input-required" : "completed";
2248
+ return !input.userAnswered && turnWaitingOnAskUser(input.lastAgentParts, input.dismissedQuestionSignature) ? "input-required" : "completed";
2235
2249
  }
2236
2250
  }
2237
2251
  var init_task_state = __esm({
@@ -22413,6 +22427,9 @@ function decisionBlockFromParts(parts, planMode, at = (/* @__PURE__ */ new Date(
22413
22427
  at
22414
22428
  );
22415
22429
  }
22430
+ function attentionOutranksAsk(existing) {
22431
+ return existing?.source === "reported" || existing?.source === "permission";
22432
+ }
22416
22433
  function withStampedQuestions(decision, existing) {
22417
22434
  if (decision.source !== "ask_user" || decision.questions?.length) return decision;
22418
22435
  if (existing?.source !== "ask_user" || !existing.questions?.length) return decision;
@@ -22426,12 +22443,37 @@ function agentBlockAfterTurn(input) {
22426
22443
  if (input.failed) return null;
22427
22444
  const decision = decisionBlockFromParts(input.parts, input.planMode);
22428
22445
  if (decision) {
22446
+ if (decision.source === "ask_user" && attentionOutranksAsk(input.existing)) {
22447
+ return input.existing ?? null;
22448
+ }
22429
22449
  const next = withStampedQuestions(decision, input.existing);
22430
22450
  if (isDismissedAskUserBlock(next, input.dismissedQuestionSignature)) {
22431
22451
  return input.existing?.source === "reported" ? input.existing : null;
22432
22452
  }
22433
22453
  return next;
22434
22454
  }
22455
+ const pending = extractPendingPlanQuestions(input.parts);
22456
+ if (pending && attentionOutranksAsk(input.existing)) {
22457
+ return input.existing ?? null;
22458
+ }
22459
+ if (pending) {
22460
+ const kept = withStampedQuestions(
22461
+ {
22462
+ ...makeAgentBlock(
22463
+ "ask_user",
22464
+ formatAskUserNotifyMessage(pending.questions),
22465
+ input.existing?.source === "ask_user" ? input.existing.at : void 0
22466
+ ),
22467
+ onScreen: true,
22468
+ questionSignature: pending.signature,
22469
+ questions: pending.questions
22470
+ },
22471
+ input.existing
22472
+ );
22473
+ if (!isDismissedAskUserBlock(kept, input.dismissedQuestionSignature)) return kept;
22474
+ } else if (input.existing?.source === "ask_user" && input.existing.onScreen && input.existing.questions?.length && !isDismissedAskUserBlock(input.existing, input.dismissedQuestionSignature)) {
22475
+ return input.existing;
22476
+ }
22435
22477
  if (input.existing && input.existing.source !== "reported" && lastTopLevelTool(input.parts)) {
22436
22478
  return null;
22437
22479
  }
@@ -22460,11 +22502,15 @@ function askBlocksEqual(a, b) {
22460
22502
  if (!a || !b) return false;
22461
22503
  return a.source === b.source && a.reason === b.reason && a.at === b.at && a.onScreen === b.onScreen && a.questionSignature === b.questionSignature;
22462
22504
  }
22463
- function askBlockAfterLiveParts(existing, parts) {
22505
+ function askBlockAfterLiveParts(existing, parts, dismissedQuestionSignature) {
22506
+ if (attentionOutranksAsk(existing)) return existing ?? null;
22507
+ if (isDismissedAskUserBlock(existing, dismissedQuestionSignature)) return null;
22508
+ const dismissed = dismissedQuestionSignature?.trim() ?? "";
22509
+ let pending = extractPendingPlanQuestions(parts);
22510
+ if (dismissed && pending?.signature === dismissed) pending = null;
22464
22511
  const last = lastTopLevelTool(parts);
22465
22512
  if (last?.type === "tool" && isAskUserToolName(last.name)) {
22466
22513
  if (existing?.source === "ask_user" && existing.onScreen) return existing;
22467
- const pending = extractPendingPlanQuestions(parts);
22468
22514
  const questions = pending?.questions ?? existing?.questions;
22469
22515
  if (!questions?.length) {
22470
22516
  return existing?.source === "ask_user" ? { ...existing, onScreen: true } : existing ?? null;
@@ -22477,13 +22523,32 @@ function askBlockAfterLiveParts(existing, parts) {
22477
22523
  questions
22478
22524
  };
22479
22525
  }
22526
+ if (pending) {
22527
+ if (existing?.source === "ask_user" && existing.onScreen && existing.questions?.length) {
22528
+ return existing;
22529
+ }
22530
+ const at = existing?.source === "ask_user" ? existing.at : void 0;
22531
+ return {
22532
+ ...makeAgentBlock("ask_user", formatAskUserNotifyMessage(pending.questions), at),
22533
+ onScreen: true,
22534
+ questionSignature: pending.signature,
22535
+ questions: pending.questions
22536
+ };
22537
+ }
22538
+ if (existing?.source === "ask_user" && existing.onScreen && existing.questions?.length) {
22539
+ return existing;
22540
+ }
22480
22541
  if (last && existing?.source === "ask_user") return null;
22481
22542
  return existing ?? null;
22482
22543
  }
22483
22544
  function syncAskUserBlockFromLive(threadId, parts) {
22484
22545
  const thread = readThread(threadId);
22485
22546
  if (!thread || thread.status === "archived") return;
22486
- const next = askBlockAfterLiveParts(thread.agentBlock, parts);
22547
+ const next = askBlockAfterLiveParts(
22548
+ thread.agentBlock,
22549
+ parts,
22550
+ thread.dismissedQuestionSignature
22551
+ );
22487
22552
  if (askBlocksEqual(thread.agentBlock, next)) return;
22488
22553
  updateThread(threadId, { agentBlock: next });
22489
22554
  }
@@ -32213,7 +32278,9 @@ ${formatEnvReminder(projectNameForRepoPath(thread.repoPath || thread.worktreePat
32213
32278
  const taskState = deriveTaskState({
32214
32279
  status: thread.status,
32215
32280
  stillRunning,
32216
- lastAgentParts: lastAgent?.parts
32281
+ lastAgentParts: lastAgent?.parts,
32282
+ dismissedQuestionSignature: thread.dismissedQuestionSignature,
32283
+ userAnswered: userRepliedAfterAgent(thread.messages, lastAgent)
32217
32284
  });
32218
32285
  const block = thread.agentBlock?.reason?.trim() ? thread.agentBlock : null;
32219
32286
  return {
@@ -45,7 +45,7 @@ import {
45
45
  syncBoardPins,
46
46
  workspaceTagTargetRef,
47
47
  writeAgentBlock
48
- } from "../chunk-YUHZEIVG.js";
48
+ } from "../chunk-XOMVOBPU.js";
49
49
  import "../chunk-DC3VZXMA.js";
50
50
  import {
51
51
  SIDEBOARD_THREAD_ID_ENV,
@@ -66,7 +66,7 @@ import {
66
66
  waitForDetachedJob,
67
67
  waitForTurnToolResult,
68
68
  workspaceAgentChatSummaries
69
- } from "../chunk-RZJ3QJZT.js";
69
+ } from "../chunk-EYDLXVED.js";
70
70
  import {
71
71
  HUGE_TOOL_RESULT_SUMMARY,
72
72
  clipToolResultForStore,
@@ -11,10 +11,10 @@ import {
11
11
  resolveSendFollowUp,
12
12
  startOrchestration,
13
13
  waitForPidExit
14
- } from "./chunk-YUHZEIVG.js";
14
+ } from "./chunk-XOMVOBPU.js";
15
15
  import "./chunk-DC3VZXMA.js";
16
16
  import "./chunk-MO46K6WK.js";
17
- import "./chunk-RZJ3QJZT.js";
17
+ import "./chunk-EYDLXVED.js";
18
18
  import "./chunk-5MXLDDYF.js";
19
19
  import "./chunk-LX5R5JOR.js";
20
20
  import "./chunk-2GEUFPQQ.js";
@@ -9,7 +9,7 @@ import {
9
9
  resolveSendFollowUp,
10
10
  startOrchestration,
11
11
  waitForPidExit
12
- } from "./chunk-DEB47BII.js";
12
+ } from "./chunk-WD222BVP.js";
13
13
  import "./chunk-ECY7WL6T.js";
14
14
  import "./chunk-PGPSIEMW.js";
15
15
  import "./chunk-W3ZLE4PO.js";
@@ -17,7 +17,7 @@ import "./chunk-NQGLDQYB.js";
17
17
  import "./chunk-KLVW3J2B.js";
18
18
  import "./chunk-S5CJUIPY.js";
19
19
  import "./chunk-ATQLP22S.js";
20
- import "./chunk-IXWPCOLQ.js";
20
+ import "./chunk-X3XTO45Y.js";
21
21
  import "./chunk-OOXRMTU6.js";
22
22
  import "./chunk-7H655NEW.js";
23
23
  import "./chunk-RDXODLEN.js";
@@ -26,7 +26,7 @@ import "./chunk-Y6OHM672.js";
26
26
  import "./chunk-QKHEVSB5.js";
27
27
  import "./chunk-RSA47QYR.js";
28
28
  import "./chunk-JAT3JFOS.js";
29
- import "./chunk-ULWPSBAB.js";
29
+ import "./chunk-NJV5YEKI.js";
30
30
  import "./chunk-N6ZF3XRP.js";
31
31
  import "./chunk-AXQ4ZWHK.js";
32
32
  import "./chunk-NURZOXGN.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sideboard-ai/core",
3
- "version": "0.1.327",
3
+ "version": "0.1.328",
4
4
  "description": "Sideboard core — orchestration, agents, git worktrees, MCP server",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",