fraim-hub 2.0.301 → 2.0.303

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
@@ -312,7 +312,7 @@ Use these defaults:
312
312
  - `feature-specification` when the request is still fuzzy or needs clarified requirements, UX, or acceptance criteria.
313
313
  - `technical-design` after the spec is approved and you need the implementation plan, file touchpoints, and risk handling.
314
314
  - `feature-implementation` for code changes, bug fixes, and documentation updates that should be executed and validated.
315
- - `test-execution` when you need reproduction coverage, missing tests, or stronger regression protection before implementation.
315
+ - `test-authoring` when you need reproduction coverage, missing tests, or stronger regression protection before implementation.
316
316
  - `browser-application-validation` or `ui-polish-validation` after user-facing UI changes or when the ask is explicitly browser validation.
317
317
  - `implementation-feature-review` when you need to verify the delivered behavior matches the feature spec.
318
318
  - `implementation-design-review` when you need to verify the code matches the approved technical design.
@@ -99,15 +99,15 @@ function normalizeCommand(value) {
99
99
  timeoutMs: typeof value.timeoutMs === 'number' && value.timeoutMs > 0 ? Math.min(value.timeoutMs, 120_000) : 30_000,
100
100
  };
101
101
  }
102
- function synthesizeDefaultConfiguredAgents(employees) {
102
+ function synthesizeDefaultConfiguredAgents(hosts) {
103
103
  const timestamp = '1970-01-01T00:00:00.000Z';
104
- return employees
105
- .filter((employee) => employee.available)
106
- .map((employee) => ({
107
- id: `${employee.id}-default`,
108
- label: employee.label,
104
+ return hosts
105
+ .filter((host) => host.available)
106
+ .map((host) => ({
107
+ id: `${host.id}-default`,
108
+ label: host.label,
109
109
  description: 'Default local launch for this agent tool.',
110
- baseHostId: employee.id,
110
+ baseHostId: host.id,
111
111
  enabled: true,
112
112
  createdAt: timestamp,
113
113
  updatedAt: timestamp,
@@ -133,9 +133,9 @@ class AiHubConfiguredAgentStore {
133
133
  return [];
134
134
  }
135
135
  }
136
- listWithDefaults(employees) {
136
+ listWithDefaults(hosts) {
137
137
  const persisted = this.list();
138
- const defaults = synthesizeDefaultConfiguredAgents(employees);
138
+ const defaults = synthesizeDefaultConfiguredAgents(hosts);
139
139
  const seen = new Set(persisted.map((agent) => agent.id));
140
140
  return [...persisted, ...defaults.filter((agent) => !seen.has(agent.id))];
141
141
  }
@@ -166,14 +166,14 @@ class AiHubConfiguredAgentStore {
166
166
  }
167
167
  }
168
168
  exports.AiHubConfiguredAgentStore = AiHubConfiguredAgentStore;
169
- function checkConfiguredAgentAvailability(agent, employees, env = process.env) {
169
+ function checkConfiguredAgentAvailability(agent, hosts, env = process.env) {
170
170
  const reasons = [];
171
171
  const warnings = [];
172
- const employee = employees.find((entry) => entry.id === agent.baseHostId);
172
+ const host = hosts.find((entry) => entry.id === agent.baseHostId);
173
173
  if (!agent.enabled)
174
174
  reasons.push('Configured agent is disabled.');
175
- if (!employee?.available)
176
- reasons.push(`${employee?.label || agent.baseHostId} is not available on this machine.`);
175
+ if (!host?.available)
176
+ reasons.push(`${host?.label || agent.baseHostId} is not available on this machine.`);
177
177
  if (agent.command) {
178
178
  if (!agent.command.command.trim()) {
179
179
  reasons.push('Command is empty.');
@@ -202,8 +202,8 @@ function checkConfiguredAgentAvailability(agent, employees, env = process.env) {
202
202
  }
203
203
  return { id: agent.id, label: agent.label, baseHostId: agent.baseHostId, enabled: agent.enabled, available: reasons.length === 0, reasons, warnings };
204
204
  }
205
- function checkConfiguredAgentReadiness(agent, employees, env = process.env) {
206
- const check = checkConfiguredAgentAvailability(agent, employees, env);
205
+ function checkConfiguredAgentReadiness(agent, hosts, env = process.env) {
206
+ const check = checkConfiguredAgentAvailability(agent, hosts, env);
207
207
  if (!check.available)
208
208
  return check;
209
209
  if (agent.command)
@@ -225,8 +225,8 @@ function checkConfiguredAgentReadiness(agent, employees, env = process.env) {
225
225
  };
226
226
  }
227
227
  }
228
- function projectConfiguredAgent(agent, employees) {
229
- const check = checkConfiguredAgentAvailability(agent, employees);
228
+ function projectConfiguredAgent(agent, hosts) {
229
+ const check = checkConfiguredAgentAvailability(agent, hosts);
230
230
  return {
231
231
  id: agent.id,
232
232
  label: agent.label,
@@ -689,6 +689,13 @@ class AiHubConversationStore {
689
689
  const { headers } = this.loadIndex(this.bucketDir(key), key);
690
690
  return headers.map((h) => ({ ...h }));
691
691
  }
692
+ // Issue #1477: the active conversation id, from the same cheap index used by
693
+ // loadProjectHeaders, so a headers-only list response can still report it.
694
+ loadProjectActiveId(projectPath) {
695
+ this.ensureMigrated();
696
+ const key = normalizeConversationKey(projectPath);
697
+ return this.loadIndex(this.bucketDir(key), key).activeId;
698
+ }
692
699
  // Lazy body read: one full conversation.
693
700
  loadConversation(projectPath, conversationId) {
694
701
  this.ensureMigrated();
@@ -57,6 +57,15 @@ class HostSessionState {
57
57
  const fallback = Object.values(sessions).find((session) => this.isResumableForOwner(session, owner));
58
58
  if (fallback)
59
59
  return fallback;
60
+ // Issue #1515: once this conversation has ANY owner-qualified session recorded, that
61
+ // ledger is authoritative. The legacy `sessionId` mirror always reflects whichever
62
+ // configured agent last ran (see `mergeFromRun` below), not the requested owner, so
63
+ // treating it as resumable here would resume a different agent's session under the
64
+ // wrong owner (e.g. Codex Azure Script silently reusing a plain Codex session because
65
+ // both share `baseHostId: 'codex'`). The legacy fallback below is reserved for
66
+ // conversations that predate the owner-qualified ledger entirely.
67
+ if (Object.keys(sessions).length > 0)
68
+ return null;
60
69
  const legacySessionId = typeof conversation.sessionId === 'string' ? conversation.sessionId.trim() : '';
61
70
  if (!legacySessionId)
62
71
  return null;
@@ -5,7 +5,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.createHubEvent = exports.createHubMessage = exports.ScriptedHostRuntime = exports.FakeHostRuntime = exports.CliHostRuntime = exports.COMPACT_TASK_FAILURE_PATTERN = void 0;
7
7
  exports.parseSeekMentoringSignal = parseSeekMentoringSignal;
8
+ exports.parseRecurringParkSignal = parseRecurringParkSignal;
8
9
  exports.parseFraimJobLoadSignal = parseFraimJobLoadSignal;
10
+ exports.parseExecutionModeSignal = parseExecutionModeSignal;
9
11
  exports.parseUsageSignal = parseUsageSignal;
10
12
  exports.parseAgentIdentitySignal = parseAgentIdentitySignal;
11
13
  exports.escapeWindowsArg = escapeWindowsArg;
@@ -16,6 +18,7 @@ exports.__clearEmployeeDetectionMemoryCacheForTests = __clearEmployeeDetectionMe
16
18
  exports.__setEmployeeDetectionTtlForTests = __setEmployeeDetectionTtlForTests;
17
19
  exports.__getEmployeeProbeRoundsForTests = __getEmployeeProbeRoundsForTests;
18
20
  exports.__resetEmployeeProbeRoundsForTests = __resetEmployeeProbeRoundsForTests;
21
+ exports.isEmployeeDetectionRefreshing = isEmployeeDetectionRefreshing;
19
22
  exports.detectEmployeesAsync = detectEmployeesAsync;
20
23
  exports.detectEmployees = detectEmployees;
21
24
  exports.prepareCodexBrowserHome = prepareCodexBrowserHome;
@@ -123,6 +126,41 @@ function parseSeekMentoringSignal(line) {
123
126
  }
124
127
  return null;
125
128
  }
129
+ // Issue #1509: detect the recurring-park marker emitted by the mentor inside its
130
+ // tool_result when it resolves a phase transition to WAITING_FOR_NEXT_INVOCATION.
131
+ // Scoped to genuine tool-result-shaped sub-objects — not a bare substring match —
132
+ // so any job that prints ai-mentor.ts source in an assistant-text message does NOT
133
+ // accidentally set the flag. A plain assistant message carries no sub-object whose
134
+ // type matches the tool-result pattern, so it is naturally excluded.
135
+ //
136
+ // Detection property: the marker must appear inside a JSON sub-object whose own
137
+ // `type` field matches the tool-result-type pattern. This covers all known host
138
+ // shapes (Claude Code tool_result, Codex function_call_output/mcp_tool_result,
139
+ // Copilot tool.execution_complete) and future hosts without per-host branches.
140
+ const TOOL_RESULT_TYPE_RE = /tool_result|function_call_output|mcp_tool_result|tool\.execution_complete/i;
141
+ function containsMarkerInToolResult(value) {
142
+ if (Array.isArray(value))
143
+ return value.some(item => containsMarkerInToolResult(item));
144
+ if (typeof value !== 'object' || value === null)
145
+ return false;
146
+ const obj = value;
147
+ if (typeof obj.type === 'string' && TOOL_RESULT_TYPE_RE.test(obj.type)) {
148
+ return JSON.stringify(obj).includes('FRAIM_RECURRING_PARK');
149
+ }
150
+ return Object.values(obj).some(v => containsMarkerInToolResult(v));
151
+ }
152
+ function parseRecurringParkSignal(line) {
153
+ if (!line.includes('FRAIM_RECURRING_PARK'))
154
+ return false;
155
+ let parsed;
156
+ try {
157
+ parsed = JSON.parse(line);
158
+ }
159
+ catch {
160
+ return false;
161
+ }
162
+ return containsMarkerInToolResult(parsed);
163
+ }
126
164
  // Issue #710: extract the job name from a get_fraim_job tool call. This
127
165
  // is structured known-job evidence; the server still validates the job id
128
166
  // against the catalog before promoting any run.
@@ -183,6 +221,43 @@ function parseFraimJobLoadSignal(line) {
183
221
  }
184
222
  return null;
185
223
  }
224
+ function parseExecutionModeSignal(line) {
225
+ if (!line.includes('Execution Mode Context'))
226
+ return null;
227
+ let parsed;
228
+ try {
229
+ parsed = JSON.parse(line);
230
+ }
231
+ catch {
232
+ return null;
233
+ }
234
+ if (typeof parsed !== 'object' || parsed === null)
235
+ return null;
236
+ const obj = parsed;
237
+ // Codex/codex_apps shape: the completed get_fraim_job tool call carries its
238
+ // result in item.result.content[].text. The local proxy side-channel does not
239
+ // run for this connector path, so Hub must consume the same payload directly.
240
+ if (obj.type === 'item.completed' &&
241
+ typeof obj.item === 'object' &&
242
+ obj.item !== null) {
243
+ const item = obj.item;
244
+ if (item.type === 'mcp_tool_call' && isFraimTool(item.tool, 'get_fraim_job')) {
245
+ const result = item.result;
246
+ const content = result && Array.isArray(result.content) ? result.content : [];
247
+ for (const entry of content) {
248
+ if (typeof entry !== 'object' || entry === null)
249
+ continue;
250
+ const text = entry.text;
251
+ if (typeof text !== 'string')
252
+ continue;
253
+ const signal = readExecutionModeFromText(text);
254
+ if (signal)
255
+ return signal;
256
+ }
257
+ }
258
+ }
259
+ return null;
260
+ }
186
261
  // Issue #347 — extract per-turn usage from the host's JSON stream.
187
262
  // Codex: `{"type":"turn.completed","usage":{input_tokens, cached_input_tokens, output_tokens, reasoning_output_tokens}}`.
188
263
  // Claude Code: `{"type":"result", ..., "usage":{input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens}, "total_cost_usd": ...}`.
@@ -476,6 +551,29 @@ function readFraimJobFromArgs(rawArgs) {
476
551
  return null;
477
552
  return { jobId, source: 'get_fraim_job' };
478
553
  }
554
+ function readExecutionModeFromText(text) {
555
+ const marker = '**Execution Mode Context:**';
556
+ const idx = text.indexOf(marker);
557
+ if (idx < 0)
558
+ return null;
559
+ const jsonStart = text.indexOf('```json', idx);
560
+ const jsonEnd = text.indexOf('```', jsonStart + 7);
561
+ if (jsonStart < 0 || jsonEnd < 0)
562
+ return null;
563
+ try {
564
+ const parsed = JSON.parse(text.slice(jsonStart + 7, jsonEnd).trim());
565
+ const mode = parsed.executionMode?.mode;
566
+ if (typeof mode !== 'string')
567
+ return null;
568
+ return {
569
+ mode: mode === 'trusted' ? 'trusted' : 'coached',
570
+ completedRuns: typeof parsed.executionMode?.completedRuns === 'number' ? parsed.executionMode.completedRuns : 0,
571
+ };
572
+ }
573
+ catch {
574
+ return null;
575
+ }
576
+ }
479
577
  function normalizeToolArgs(rawArgs) {
480
578
  if (rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)) {
481
579
  return rawArgs;
@@ -895,10 +993,11 @@ function resolveHostInvocation(plan) {
895
993
  args: ['/d', '/s', '/c', [command, ...args].map(escapeWindowsArg).join(' ')],
896
994
  };
897
995
  }
996
+ // Issue #1256 (slice a, AC-A3): delegates to the shared recovery helper so this probe agrees
997
+ // with server.ts's hubCommandVersion() and fraim doctor's health check on the same PATH,
998
+ // instead of each independently deciding what "the agent's PATH" means.
898
999
  function buildAgentVersionProbePath(basePath) {
899
- const withoutManaged = (0, managed_agent_paths_1.stripManagedAgentBinDirsFromPath)(basePath);
900
- const withoutProjectBins = (0, managed_agent_paths_1.stripProjectLocalNodeBinDirs)(withoutManaged);
901
- return (0, managed_agent_paths_1.appendBinDirsToPath)(withoutProjectBins, (0, managed_agent_paths_1.getManagedAgentBinDirs)());
1000
+ return (0, managed_agent_paths_1.buildRecoveredAgentPath)(basePath);
902
1001
  }
903
1002
  // Single source for the probe environment, shared by the sync and async probes so the two
904
1003
  // cannot drift on how agent bin directories are put on PATH. Project-local npm shims are
@@ -918,7 +1017,10 @@ const availableByVersionProbe = (command) => {
918
1017
  });
919
1018
  if (result.status !== 0 || result.error)
920
1019
  return null;
921
- return (result.stdout || result.stderr || '').trim() || null;
1020
+ // Issue #1256 (AC-D1/D2): only stdout counts as a version an agent that exits 0 while
1021
+ // writing only to stderr (e.g. GitHub Copilot CLI's current failure mode) is broken, not
1022
+ // "available with an odd version string".
1023
+ return (0, managed_agent_paths_1.versionFromProbeOutput)(result.stdout, result.stderr);
922
1024
  };
923
1025
  // Issue #1010: the async counterpart of availableByVersionProbe. Same semantics (exit 0
924
1026
  // from `<agent> --version` means the CLI is installed AND actually runs), but non-blocking
@@ -937,7 +1039,11 @@ const availableByVersionProbeAsync = (command) => new Promise((resolve) => {
937
1039
  let settled = false;
938
1040
  let timer;
939
1041
  let child;
940
- const chunks = [];
1042
+ // Issue #1256 (AC-D1/D2): kept as separate stdout/stderr buffers rather than one combined
1043
+ // buffer, so a broken CLI that exits 0 while writing only to stderr cannot have its error
1044
+ // text mistaken for a version string.
1045
+ const stdoutChunks = [];
1046
+ const stderrChunks = [];
941
1047
  const finish = (value) => {
942
1048
  if (settled)
943
1049
  return;
@@ -951,8 +1057,8 @@ const availableByVersionProbeAsync = (command) => new Promise((resolve) => {
951
1057
  env: versionProbeEnv(),
952
1058
  stdio: ['ignore', 'pipe', 'pipe'],
953
1059
  });
954
- child.stdout?.on('data', (chunk) => chunks.push(chunk));
955
- child.stderr?.on('data', (chunk) => chunks.push(chunk));
1060
+ child.stdout?.on('data', (chunk) => stdoutChunks.push(chunk));
1061
+ child.stderr?.on('data', (chunk) => stderrChunks.push(chunk));
956
1062
  timer = setTimeout(() => {
957
1063
  console.warn(`[ai-hub] agent version probe timed out after ${VERSION_PROBE_TIMEOUT_MS}ms: ${command}`);
958
1064
  try {
@@ -965,8 +1071,9 @@ const availableByVersionProbeAsync = (command) => new Promise((resolve) => {
965
1071
  timer.unref?.();
966
1072
  child.on('error', () => finish(null));
967
1073
  child.on('close', (code) => {
968
- const raw = Buffer.concat(chunks).toString('utf8').trim();
969
- finish(code === 0 ? (raw || null) : null);
1074
+ const stdout = Buffer.concat(stdoutChunks).toString('utf8');
1075
+ const stderr = Buffer.concat(stderrChunks).toString('utf8');
1076
+ finish(code === 0 ? (0, managed_agent_paths_1.versionFromProbeOutput)(stdout, stderr) : null);
970
1077
  });
971
1078
  }
972
1079
  catch {
@@ -986,11 +1093,13 @@ const availableByVersionProbeAsync = (command) => new Promise((resolve) => {
986
1093
  // cached. The TTL is a safety net for a CLI installed outside the Hub; installs performed
987
1094
  // THROUGH the Hub invalidate explicitly (see the install-agent route) so they appear at once.
988
1095
  const EMPLOYEE_DETECTION_TTL_MS = 5 * 60 * 1000;
1096
+ const EMPLOYEE_DETECTION_REFRESH_SIGNAL_MS = 500;
989
1097
  let employeeDetectionTtlMs = EMPLOYEE_DETECTION_TTL_MS;
990
1098
  let cachedEmployees = null;
991
1099
  let cachedEmployeesAtMs = 0;
992
1100
  let cachedEmployeesContext = null;
993
1101
  let inFlightDetection = null;
1102
+ let employeeDetectionRefreshSignalUntilMs = 0;
994
1103
  function employeeDetectionContext() {
995
1104
  return JSON.stringify({
996
1105
  PATH: process.env.PATH || '',
@@ -1080,6 +1189,7 @@ function adoptPersistedEmployees(persisted) {
1080
1189
  cachedEmployeesAtMs = persisted.detectedAtMs;
1081
1190
  cachedEmployeesContext = employeeDetectionContext();
1082
1191
  if (Date.now() - persisted.detectedAtMs > employeeDetectionTtlMs) {
1192
+ employeeDetectionRefreshSignalUntilMs = Date.now() + EMPLOYEE_DETECTION_REFRESH_SIGNAL_MS;
1083
1193
  void detectEmployeesAsync({ force: true }).catch(() => undefined);
1084
1194
  }
1085
1195
  return persisted.employees;
@@ -1116,6 +1226,7 @@ function invalidateEmployeeDetectionCache() {
1116
1226
  cachedEmployeesAtMs = 0;
1117
1227
  cachedEmployeesContext = null;
1118
1228
  inFlightDetection = null;
1229
+ employeeDetectionRefreshSignalUntilMs = 0;
1119
1230
  try {
1120
1231
  fs_1.default.rmSync(agentAvailabilityFilePath(), { force: true });
1121
1232
  }
@@ -1127,6 +1238,7 @@ function __clearEmployeeDetectionMemoryCacheForTests() {
1127
1238
  cachedEmployeesAtMs = 0;
1128
1239
  cachedEmployeesContext = null;
1129
1240
  inFlightDetection = null;
1241
+ employeeDetectionRefreshSignalUntilMs = 0;
1130
1242
  }
1131
1243
  /** Test seam only. Mirrors __resetLatestVersionCache in hub-latest-version.ts. */
1132
1244
  function __setEmployeeDetectionTtlForTests(ttlMs) {
@@ -1158,6 +1270,16 @@ function buildEmployeeStatus(id, version) {
1158
1270
  supportsRaw: supportsDirectPath(id),
1159
1271
  };
1160
1272
  }
1273
+ /**
1274
+ * Issue #1256 (slice b, AC-B1): true when a background re-probe is currently in flight —
1275
+ * i.e. the most recent detectEmployees()/detectEmployeesAsync() answer may have been served
1276
+ * from a stale persisted snapshot rather than a fresh probe. bootstrapResponse reads this
1277
+ * right after detection so the client can be told to poll again shortly instead of latching
1278
+ * a one-shot answer.
1279
+ */
1280
+ function isEmployeeDetectionRefreshing() {
1281
+ return inFlightDetection !== null || Date.now() < employeeDetectionRefreshSignalUntilMs;
1282
+ }
1161
1283
  /**
1162
1284
  * Non-blocking employee detection. Probes every agent CONCURRENTLY, so the cost is the
1163
1285
  * slowest single probe rather than the sum of all of them, and the event loop stays free
@@ -1677,24 +1799,29 @@ function parseHostLine(hostId, line) {
1677
1799
  return {};
1678
1800
  // Scan every line for structured signals the Hub UI cares about:
1679
1801
  // seekMentoring (tracker), get_fraim_job (job identity promotion),
1680
- // turn-level usage (totals), and fraim_connect agent identity
1681
- // (cost lookup when the host doesn't emit it).
1802
+ // turn-level usage (totals), fraim_connect agent identity
1803
+ // (cost lookup when the host doesn't emit it), and recurringPark
1804
+ // (marker emitted by the mentor when transitioning to the recurring sentinel).
1682
1805
  const seekMentoring = parseSeekMentoringSignal(trimmed);
1683
1806
  const fraimJob = parseFraimJobLoadSignal(trimmed);
1807
+ const executionMode = parseExecutionModeSignal(trimmed);
1684
1808
  const usage = parseUsageSignal(trimmed);
1685
1809
  const agentIdentity = parseAgentIdentitySignal(trimmed);
1810
+ const recurringPark = parseRecurringParkSignal(trimmed);
1686
1811
  // Issue #1264 (R3): only worth checking when seekMentoring itself did not
1687
1812
  // already resolve — a recognized call has nothing to diagnose.
1688
1813
  const unrecognizedReviewHandoffError = seekMentoring ? null : detectUnrecognizedReviewHandoffCall(trimmed);
1689
1814
  const withSignal = (event) => {
1690
- if (!seekMentoring && !fraimJob && !usage && !agentIdentity && !unrecognizedReviewHandoffError)
1815
+ if (!seekMentoring && !fraimJob && !executionMode && !usage && !agentIdentity && !recurringPark && !unrecognizedReviewHandoffError)
1691
1816
  return event;
1692
1817
  return {
1693
1818
  ...event,
1694
1819
  ...(seekMentoring ? { seekMentoring } : {}),
1695
1820
  ...(fraimJob ? { fraimJob } : {}),
1821
+ ...(executionMode ? { executionMode } : {}),
1696
1822
  ...(usage ? { usage } : {}),
1697
1823
  ...(agentIdentity ? { agentIdentity } : {}),
1824
+ ...(recurringPark ? { recurringPark: true } : {}),
1698
1825
  ...(unrecognizedReviewHandoffError && !event.hostError ? { hostError: unrecognizedReviewHandoffError } : {}),
1699
1826
  };
1700
1827
  };
@@ -2140,6 +2267,11 @@ class CliHostRuntime {
2140
2267
  detectEmployeesAsync() {
2141
2268
  return detectEmployeesAsync();
2142
2269
  }
2270
+ // Issue #1256 (slice b): lets bootstrapResponse tell the client whether the roster it just
2271
+ // served may be stale while a background re-probe lands.
2272
+ isEmployeeDetectionRefreshing() {
2273
+ return isEmployeeDetectionRefreshing();
2274
+ }
2143
2275
  startRun(hostId, projectPath, message, handlers, sessionId, launchContext) {
2144
2276
  // R11: start/startDirect mint sessions rather than resuming one, so they
2145
2277
  // stay outside the continuation queue entirely.