mixdog 1.3.0 → 1.3.1

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.
Files changed (103) hide show
  1. package/NOTICE.md +52 -9
  2. package/package.json +1 -1
  3. package/src/defaults/shipped-history.json +4 -1
  4. package/src/defaults/skills/setup/references/actions.md +4 -1
  5. package/src/defaults/skills/setup-guide/SKILL.md +10 -7
  6. package/src/output-styles/common.md +13 -8
  7. package/src/rules/agent-mode/AGENT_MODE.md +1 -1
  8. package/src/rules/agent-mode/MIXY.md +11 -4
  9. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +72 -14
  10. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +2 -1
  11. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +7 -2
  12. package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +7 -8
  13. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +3 -1
  14. package/src/runtime/agent/orchestrator/providers/lib/tool-call-wire-ids.mjs +65 -0
  15. package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +8 -2
  16. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +9 -2
  17. package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +3 -1
  18. package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +14 -2
  19. package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +17 -0
  20. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +4 -2
  21. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +14 -4
  22. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +37 -7
  23. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +5 -3
  24. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +4 -0
  25. package/src/runtime/agent/orchestrator/session/agent-room.mjs +17 -8
  26. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +32 -5
  27. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +9 -0
  28. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +108 -6
  29. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +8 -14
  30. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +46 -4
  31. package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +18 -8
  32. package/src/runtime/agent/orchestrator/session/manager/provider-replay.mjs +50 -0
  33. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +7 -20
  34. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +3 -0
  35. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +7 -1
  36. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +1 -1
  37. package/src/runtime/agent/orchestrator/session/mixy-name.mjs +5 -2
  38. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +17 -1
  39. package/src/runtime/agent/orchestrator/session/routine-result.mjs +61 -9
  40. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +14 -9
  41. package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +92 -15
  42. package/src/runtime/attachments/store.mjs +6 -0
  43. package/src/runtime/effort-judge/judge-client.mjs +1 -1
  44. package/src/runtime/effort-judge/model-manifest.json +7 -7
  45. package/src/runtime/shared/agent-kind.mjs +152 -0
  46. package/src/runtime/shared/agent-route-config.d.mts +1 -0
  47. package/src/runtime/shared/agent-route-config.mjs +22 -8
  48. package/src/runtime/shared/agent-store.mjs +132 -0
  49. package/src/runtime/shared/bot-split-migration.mjs +457 -0
  50. package/src/runtime/shared/config-patch.mjs +20 -1
  51. package/src/runtime/shared/connector-catalog.d.mts +1 -2
  52. package/src/runtime/shared/connector-catalog.mjs +2 -4
  53. package/src/runtime/shared/connector-triggers.mjs +2 -0
  54. package/src/runtime/shared/err-text.mjs +3 -1
  55. package/src/runtime/shared/error-presentation.mjs +91 -6
  56. package/src/runtime/shared/handoff-artifacts.d.mts +20 -0
  57. package/src/runtime/shared/handoff-artifacts.mjs +61 -0
  58. package/src/runtime/shared/llm/provider-model-identities.mjs +6 -1
  59. package/src/runtime/shared/llm/usage-accounting.mjs +7 -1
  60. package/src/runtime/shared/model-route-fallback.mjs +79 -7
  61. package/src/runtime/shared/plain-preview.mjs +22 -17
  62. package/src/runtime/shared/sensitive-path-rules.d.mts +15 -0
  63. package/src/runtime/shared/sensitive-path-rules.mjs +256 -0
  64. package/src/runtime/shared/usage-readout-totals.mjs +15 -13
  65. package/src/runtime/shared/user-data-guard.mjs +5 -2
  66. package/src/session-runtime/agent-home-delivery.mjs +423 -56
  67. package/src/session-runtime/agent-room-delivery.mjs +45 -4
  68. package/src/session-runtime/agent-task-sessions.mjs +299 -39
  69. package/src/session-runtime/boot/apis.mjs +13 -0
  70. package/src/session-runtime/boot/providers.mjs +14 -4
  71. package/src/session-runtime/config-lifecycle.mjs +43 -2
  72. package/src/session-runtime/handoff-attachments.mjs +593 -0
  73. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +27 -3
  74. package/src/session-runtime/model-route/route-persist.mjs +2 -2
  75. package/src/session-runtime/model-route/route-tuning.mjs +7 -1
  76. package/src/session-runtime/model-route/set-route.mjs +21 -1
  77. package/src/session-runtime/native-web-search.mjs +2 -2
  78. package/src/session-runtime/new-session-config.mjs +6 -1
  79. package/src/session-runtime/route-resolution.mjs +5 -11
  80. package/src/session-runtime/services/agent-tool/helpers.mjs +24 -5
  81. package/src/session-runtime/services/agent-tool/send-flow.mjs +11 -3
  82. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +4 -1
  83. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +5 -8
  84. package/src/session-runtime/services/agent-tool/tool-def.mjs +7 -0
  85. package/src/session-runtime/setup-tool/executor.mjs +12 -1
  86. package/src/session-runtime/setup-tool/extended-actions.mjs +19 -7
  87. package/src/session-runtime/setup-tool/settings-contract.mjs +6 -0
  88. package/src/session-runtime/tool-policy-refresh.mjs +13 -2
  89. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +11 -0
  90. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +123 -32
  91. package/src/session-runtime/workflow-agents-api/agent-route.mjs +67 -7
  92. package/src/session-runtime/workflow-agents-api/onboarding.mjs +11 -4
  93. package/src/standalone/daemon-session-control.mjs +13 -5
  94. package/src/standalone/daemon.mjs +16 -4
  95. package/src/standalone/session-service/agent-tree/agent-turns.mjs +46 -3
  96. package/src/standalone/session-service/agent-tree.mjs +4 -3
  97. package/src/standalone/session-service/session-calls/session-turns.mjs +19 -0
  98. package/src/tui/dist/index.mjs +70 -9
  99. package/src/tui/session/notice-surface.mjs +3 -4
  100. package/src/tui/session/session-api/routes.mjs +14 -3
  101. package/src/tui/session/transcript-intake.mjs +2 -0
  102. package/src/tui/session/turn-live-call.mjs +11 -3
  103. package/src/tui/session/turn.mjs +13 -3
@@ -10,7 +10,7 @@
10
10
  import { createAgentCancellation } from './agent-tree/agent-cancel.mjs';
11
11
  import { createAgentRegistry } from './agent-tree/agent-registry.mjs';
12
12
  import { createAgentRehydration, lastStoredAgentHandoff } from './agent-tree/agent-rehydrate.mjs';
13
- import { createAgentTurns } from './agent-tree/agent-turns.mjs';
13
+ import { agentTurnContent, agentTurnTranscriptMeta, createAgentTurns } from './agent-tree/agent-turns.mjs';
14
14
  import { randomUUID } from 'node:crypto';
15
15
 
16
16
  export { SESSION_ID_PATTERN } from './agent-tree/agent-registry.mjs';
@@ -139,12 +139,13 @@ export function createAgentTree({
139
139
  const cancelEpoch = registry.assertAdmission(descriptor.id);
140
140
  const runtime = sessionOwner(descriptor.id)?.runtime;
141
141
  if (!runtime || !stateBusy(runtime.getState())) return null;
142
- const accepted = await runtime.submitAsync(String(prompt || ''), {
142
+ const content = agentTurnContent(prompt);
143
+ const accepted = await runtime.submitAsync(content, {
143
144
  id: `agent-message-${randomUUID()}`,
144
145
  mode: 'prompt',
145
146
  priority: 'next',
146
147
  context,
147
- transcriptMeta: { sender: 'lead' },
148
+ transcriptMeta: agentTurnTranscriptMeta(content),
148
149
  });
149
150
  if (accepted === false) throw new Error('agent follow-up was not accepted');
150
151
  registry.assertAdmission(descriptor.id, cancelEpoch);
@@ -12,6 +12,7 @@ import {
12
12
  import { providerNativePdf } from '../../../runtime/agent/orchestrator/providers/registry.mjs';
13
13
  import { cancelBackgroundTasks } from '../../../runtime/shared/background-tasks.mjs';
14
14
  import { isAgentModeSession } from '../../../runtime/agent/orchestrator/session/agent-mode.mjs';
15
+ import { isAgentHomeSessionId } from '../../../runtime/agent/orchestrator/session/agent-home.mjs';
15
16
  import { recognisedAgentRoomSlash } from '../../../tui/app/slash-commands.mjs';
16
17
  import { promptContentText } from '../../../tui/session/queue-helpers.mjs';
17
18
 
@@ -38,6 +39,17 @@ function abortsAgentTurn(id, state, hasAgentSession) {
38
39
  );
39
40
  }
40
41
 
42
+ /** A prompt the user typed: not an agent's send (`sender: 'lead'`), a hidden
43
+ * wake, a routine fire or any other runtime-made turn. */
44
+ export function isUserTurn(options = {}) {
45
+ return (
46
+ (options.mode || 'prompt') === 'prompt' &&
47
+ options.kickoff !== true &&
48
+ options.isMeta !== true &&
49
+ options.transcriptMeta?.sender !== 'lead'
50
+ );
51
+ }
52
+
41
53
  export function createSessionTurnCalls(ctx) {
42
54
  const { log, readStoredGoal, listStoredActiveGoalSessionIds, hasAgentSession, sessionResult, advanceForCaller } = ctx;
43
55
  const { retainUnwatched } = ctx.retention;
@@ -88,6 +100,13 @@ export function createSessionTurnCalls(ctx) {
88
100
  const accepted = await Promise.resolve(target.call(entry.runtime, intake.prompt, submissionOptions));
89
101
  if (accepted === true) {
90
102
  entry.reservedOnly = false;
103
+ if (isAgentHomeSessionId(id) && isUserTurn(submissionOptions)) {
104
+ // The user spoke in this agent's home: its round trips with other
105
+ // sessions count from zero again, so a capped thread wakes again.
106
+ void import('../../../session-runtime/agent-home-delivery.mjs')
107
+ .then(({ resetAgentTaskRounds }) => resetAgentTaskRounds(id))
108
+ .catch((error) => log(`round reset of ${id} failed: ${error?.message || error}`));
109
+ }
91
110
  }
92
111
  const step = settleAction(entry, 'headless session submit');
93
112
  log(`session submit session=${id} accepted=${accepted === true}`);
@@ -42465,8 +42465,19 @@ var CODE_KINDS = new Map(
42465
42465
  quota_exhausted: ["quota_exceeded", "quota_exhausted", "usage_limit_reached", "provider_accounts_exhausted"],
42466
42466
  context_overflow: ["context_length_exceeded", "context_window_exceeded", "model_context_window_exceeded"],
42467
42467
  request_too_large: ["request_too_large", "payload_too_large", "request_entity_too_large"],
42468
- auth: ["invalid_api_key", "authentication_error", "unauthenticated", "unauthorized", "invalid_authentication"],
42469
- permission: ["permission_error", "permission_denied", "forbidden"],
42468
+ // Google names a rejected or expired key with an ErrorInfo reason on an
42469
+ // INVALID_ARGUMENT 400; the reason outranks the envelope status.
42470
+ auth: [
42471
+ "invalid_api_key",
42472
+ "authentication_error",
42473
+ "unauthenticated",
42474
+ "unauthorized",
42475
+ "invalid_authentication",
42476
+ "api_key_invalid",
42477
+ "api_key_expired",
42478
+ "access_token_expired"
42479
+ ],
42480
+ permission: ["permission_error", "permission_denied", "forbidden", "service_disabled", "api_key_service_blocked"],
42470
42481
  overloaded: [
42471
42482
  "server_is_overloaded",
42472
42483
  "overloaded_error",
@@ -42504,6 +42515,7 @@ var CODE_KINDS = new Map(
42504
42515
  "not_found",
42505
42516
  "not_found_error",
42506
42517
  "model_not_found",
42518
+ "model_not_supported",
42507
42519
  "unsupported_parameter"
42508
42520
  ]
42509
42521
  }).flatMap(([kind, codes]) => codes.map((code) => [code, kind]))
@@ -42854,6 +42866,42 @@ function resetLabel(resetAt) {
42854
42866
  const time = `${pad(reset.getHours())}:${pad(reset.getMinutes())}`;
42855
42867
  return reset.toDateString() === (/* @__PURE__ */ new Date()).toDateString() ? time : `${reset.getMonth() + 1}/${reset.getDate()} ${time}`;
42856
42868
  }
42869
+ var MODEL_REFUSAL_KINDS = /* @__PURE__ */ new Set(["invalid_request", "permission", "unknown"]);
42870
+ var MODEL_UNAVAILABLE_CODES = /* @__PURE__ */ new Set(["model_not_found", "model_not_supported"]);
42871
+ var MODEL_REFUSAL_PREDICATE = "(?:(?:is|was|has been)\\s+)?(?:not found|does not exist|doesn'?t exist|not (?:supported|available)|(?:currently\\s+)?unavailable|no longer (?:available|supported)|deprecated|retired|removed)\\b";
42872
+ var SENTENCE_START = "(?:^|[.:;!]\\s+)";
42873
+ var MODEL_UNAVAILABLE_RE = new RegExp(
42874
+ [
42875
+ // "The model `x` does not exist", "models/x is not found", Vertex
42876
+ // "Publisher Model `…` was not found", Ollama `model "x" not found`.
42877
+ `${SENTENCE_START}(?:(?:the|this|that|requested|selected|publisher)\\s+)*models?(?:\\/\\S+|\\s+[\`'"]?[\\w./:@-]+[\`'"]?)?\\s+${MODEL_REFUSAL_PREDICATE}`,
42878
+ // OpenAI "The 'gpt-x' model is not supported when using Codex …".
42879
+ `${SENTENCE_START}(?:the\\s+)?[\`'"][^\`'"\\n]+[\`'"]\\s+model\\s+${MODEL_REFUSAL_PREDICATE}`,
42880
+ `${SENTENCE_START}(?:unknown|invalid|unsupported) model\\b`,
42881
+ // OpenRouter: no provider serves this model id.
42882
+ `${SENTENCE_START}no endpoints found for [\\w.-]+\\/[\\w.:-]+`,
42883
+ "\\bis not a valid model\\b",
42884
+ "^model: \\S+$"
42885
+ ].join("|"),
42886
+ "im"
42887
+ );
42888
+ var MODEL_UNAVAILABLE_COPY = [
42889
+ "model-unavailable",
42890
+ "This model isn't available for this account.",
42891
+ "Choose another model, or check the model and endpoint in Providers."
42892
+ ];
42893
+ var NOT_FOUND_SUMMARY = "The provider couldn't find the requested model or endpoint.";
42894
+ var TOOL_SCHEMA_RE = /invalid schema for (?:function|tool)\b|\btools\.\d+\.(?:custom\.)?input_schema\b|\bfunction_declarations\[\d+\]\.parameters\b|\btools\[\d+\]\.function\.parameters\b|\binvalid (?:tool|function) (?:schema|parameters|definition)\b/i;
42895
+ var TOOL_SCHEMA_COPY = [
42896
+ "tool-schema",
42897
+ "The provider rejected a tool definition.",
42898
+ "Disable the tool or MCP server named in the details, then try again."
42899
+ ];
42900
+ function firstReadableLine(clean3) {
42901
+ const lines = clean3.split("\n").map((line) => line.trim()).filter(Boolean);
42902
+ const first = lines.find((line) => !/^at\s|^\{|^<!doctype|^<html/i.test(line)) || "";
42903
+ return first && !/^[{[]|^<|(?:API|HTTP)\s+[45]\d\d.*[<{]/i.test(first) ? first : "";
42904
+ }
42857
42905
  var LEGACY_TEXT_KINDS = [
42858
42906
  [/The provider usage limit was reached|quota.*(?:hit|exceed)|usage limit/i, "quota_exhausted"],
42859
42907
  [/rate[_ -]?limit|too many requests/i, "rate_limited"]
@@ -42921,6 +42969,14 @@ function describeError(value) {
42921
42969
  kind = "image-size";
42922
42970
  summary = "An attached image exceeds the provider limit.";
42923
42971
  recovery = "Reduce the image dimensions or number of images, then try again.";
42972
+ } else if (clean3 === MODEL_UNAVAILABLE_COPY[1] || MODEL_REFUSAL_KINDS.has(descriptor.kind) && (MODEL_UNAVAILABLE_CODES.has(code) || MODEL_UNAVAILABLE_RE.test(clean3))) {
42973
+ [kind, summary, recovery] = MODEL_UNAVAILABLE_COPY;
42974
+ } else if (clean3 === TOOL_SCHEMA_COPY[1] || ["invalid_request", "unknown"].includes(descriptor.kind) && TOOL_SCHEMA_RE.test(clean3)) {
42975
+ [kind, summary, recovery] = TOOL_SCHEMA_COPY;
42976
+ } else if (clean3 === NOT_FOUND_SUMMARY || MODEL_REFUSAL_KINDS.has(descriptor.kind) && (status === 404 || status === 410)) {
42977
+ kind = "not-found";
42978
+ summary = firstReadableLine(clean3) || NOT_FOUND_SUMMARY;
42979
+ recovery = MODEL_UNAVAILABLE_COPY[2];
42924
42980
  } else if (descriptor.kind === "unknown" && /payload too large|request (?:body )?too large|The request exceeds the size limit/i.test(clean3)) {
42925
42981
  [kind, summary, recovery] = KIND_COPY.request_too_large;
42926
42982
  } else if (copy) {
@@ -42943,9 +42999,7 @@ function describeError(value) {
42943
42999
  summary = "The provider rejected this request.";
42944
43000
  recovery = "Review the request details before trying again.";
42945
43001
  } else {
42946
- const lines = clean3.split("\n").map((line) => line.trim()).filter(Boolean);
42947
- const first = lines.find((line) => !/^at\s|^\{|^<!doctype|^<html/i.test(line)) || "";
42948
- summary = first && !/^[{[]|^<|(?:API|HTTP)\s+[45]\d\d.*[<{]/i.test(first) ? first : "Something went wrong.";
43002
+ summary = firstReadableLine(clean3) || "Something went wrong.";
42949
43003
  }
42950
43004
  summary = summary.replace(/\s+/g, " ").trim() || "Something went wrong.";
42951
43005
  if (summary.length > SUMMARY_LIMIT) summary = `${summary.slice(0, SUMMARY_LIMIT - 1).trimEnd()}\u2026`;
@@ -43157,7 +43211,9 @@ function typedStreamFailureText(error, text) {
43157
43211
  }
43158
43212
  function presentErrorText(error, options = {}) {
43159
43213
  const presentation = describeError(error);
43160
- if (["image-size", "payload-size", "request"].includes(presentation.kind)) return presentation.summary;
43214
+ if (["image-size", "payload-size", "request", "model-unavailable", "tool-schema"].includes(presentation.kind)) {
43215
+ return presentation.summary;
43216
+ }
43161
43217
  const kindLine = hasTypedEvidence(error) ? KIND_LINES[presentation.providerKind] : null;
43162
43218
  if (kindLine && presentation.kind !== "unknown") {
43163
43219
  return typeof kindLine === "function" ? kindLine(presentation) : kindLine;
@@ -50766,7 +50822,12 @@ var ANTIGRAVITY_MODELS = Object.freeze([
50766
50822
  low: "gemini-3.1-pro-low",
50767
50823
  high: "gemini-3.1-pro-high"
50768
50824
  }),
50769
- fallbackModel("gemini-4-argon", "Gemini 4 Argon", "gemini-pro", 1048576, "gemini-4-argon", ["low", "medium", "high"]),
50825
+ // The gateway serves Argon only through per-tier ids; bare `gemini-4-argon` is a 404.
50826
+ fallbackModel("gemini-4-argon", "Gemini 4 Argon", "gemini-pro", 1048576, {
50827
+ low: "gemini-4-argon-low",
50828
+ medium: "gemini-4-argon-medium",
50829
+ high: "gemini-4-argon-high"
50830
+ }),
50770
50831
  fallbackModel("claude-opus-4-6-thinking", "Claude Opus 4.6 (Thinking)", null, 25e4, "claude-opus-4-6-thinking", [
50771
50832
  "low",
50772
50833
  "medium",
@@ -61755,7 +61816,7 @@ function finishBackup(dataDir2, backupDir, reason) {
61755
61816
  restoreLookupCache.delete(normalizeDataDir(dataDir2));
61756
61817
  legacyEvidenceCache.delete(normalizeDataDir(dataDir2));
61757
61818
  }
61758
- function backupUserData(dataDir2, reason = "snapshot") {
61819
+ function backupUserData(dataDir2, reason = "snapshot", { dirs = [] } = {}) {
61759
61820
  if (skipBackups()) return { dir: null, copied: [] };
61760
61821
  if (!dataDir2 || !existsSync10(dataDir2)) return { dir: null, copied: [] };
61761
61822
  const scopeDir = scopeDirFor(dataDir2);
@@ -61765,7 +61826,7 @@ function backupUserData(dataDir2, reason = "snapshot") {
61765
61826
  const src = join11(dataDir2, rel);
61766
61827
  if (existsSync10(src)) copyTree(src, join11(backupDir, rel), copied);
61767
61828
  }
61768
- for (const rel of USER_DATA_DIRS) {
61829
+ for (const rel of [...USER_DATA_DIRS, ...dirs]) {
61769
61830
  const src = join11(dataDir2, rel);
61770
61831
  if (existsSync10(src)) copyTree(src, join11(backupDir, rel), copied);
61771
61832
  }
@@ -59,10 +59,9 @@ export function createNoticeSurface({ getState, set, isDisposed, pushItem, repla
59
59
  // A schedule result posted into a home thread (routine-result.mjs).
60
60
  ...(options.routine ? { routine: options.routine } : {}),
61
61
  ...(options.room ? { room: options.room } : {}),
62
- // A room user row's files, in the fields a prompt row draws them from.
63
- ...(userRow && Array.isArray(options.attachments) && options.attachments.length
64
- ? { attachments: options.attachments }
65
- : {}),
62
+ // A room user row's files, in the fields a prompt row draws them from;
63
+ // a task report's or member speech's returned files likewise.
64
+ ...(Array.isArray(options.attachments) && options.attachments.length ? { attachments: options.attachments } : {}),
66
65
  ...(userRow && Array.isArray(options.images) && options.images.length ? { images: options.images } : {}),
67
66
  // A held request of an unattended run (human-gate/held-message.mjs).
68
67
  ...(options.heldRequest ? { heldRequest: options.heldRequest } : {}),
@@ -131,7 +131,10 @@ export function createSessionRouteApi(bag) {
131
131
  // is the only place the display is touched once the write has started.
132
132
  const originGeneration = runtime.sessionGeneration;
133
133
  const owned = () => runtime.sessionGeneration === originGeneration;
134
- const preview = optimisticRoutePatch(previewedRequest(routeOpts), getState());
134
+ // A conditional fill (only when the route is still empty) may not apply, so
135
+ // it never previews: the runtime's answer is what gets published.
136
+ const onlyIfEmpty = routeOpts.onlyIfEmpty === true;
137
+ const preview = onlyIfEmpty ? null : optimisticRoutePatch(previewedRequest(routeOpts), getState());
135
138
  // Preview immediately, serialize persistence without dropping a later
136
139
  // click. Earlier completions cannot replace the newest visible choice.
137
140
  if (!getState().commandBusy) routeLocked = true;
@@ -159,7 +162,13 @@ export function createSessionRouteApi(bag) {
159
162
  const applyToCurrentSession = routeOpts.applyToCurrentSession === true;
160
163
  const keepMainModel = routeOpts.keepMainModel !== false;
161
164
  const sessionOnly = routeOpts.sessionOnly === true;
162
- const { applyToCurrentSession: _drop, keepMainModel: _keep, sessionOnly: _scope, ...requested } = routeOpts;
165
+ const {
166
+ applyToCurrentSession: _drop,
167
+ keepMainModel: _keep,
168
+ sessionOnly: _scope,
169
+ onlyIfEmpty: _fill,
170
+ ...requested
171
+ } = routeOpts;
163
172
  let nextRoute = requested;
164
173
  if (sessionOnly) {
165
174
  const completed = completeSessionOnlyRequest(requested, previousRoute);
@@ -175,6 +184,7 @@ export function createSessionRouteApi(bag) {
175
184
  applyToCurrentSession,
176
185
  keepMainModel,
177
186
  ...(sessionOnly ? { sessionOnly } : {}),
187
+ ...(onlyIfEmpty ? { onlyIfEmpty } : {}),
178
188
  ...(originGeneration !== undefined ? { originGeneration } : {}),
179
189
  switchEstablished: true,
180
190
  deferUntilNextTurn: true,
@@ -300,7 +310,8 @@ export function createSessionRouteApi(bag) {
300
310
  saveLeadPersona: async (text) => runtime.saveLeadPersona?.(text) ?? '',
301
311
  getLeadAgentBody: () => runtime.getLeadAgentBody?.() ?? '',
302
312
  saveLeadAgentBody: async (text) => runtime.saveLeadAgentBody?.(text) ?? '',
303
- getLeadStatus: () => runtime.getLeadStatus?.() ?? { created: null, name: '', avatar: null },
313
+ getLeadStatus: () =>
314
+ runtime.getLeadStatus?.() ?? { created: null, name: '', avatar: null, homeChatted: false, route: null },
304
315
  saveLeadIdentity: async (payload) => runtime.saveLeadIdentity?.(payload) ?? null,
305
316
  setLeadCreated: async (created, description) => runtime.setLeadCreated?.(created, description) ?? null,
306
317
  saveAgentDefinition: async (payload) => runtime.saveAgentDefinition?.(payload) ?? null,
@@ -123,6 +123,8 @@ export function createTranscriptIntake({ getState, flags, pushItem, patchItem, t
123
123
  ...routineTriggerField(extra),
124
124
  ...(extra && typeof extra.sender === 'string' && extra.sender ? { sender: extra.sender } : {}),
125
125
  ...(extra && typeof extra.device === 'string' && extra.device ? { device: extra.device } : {}),
126
+ // Delivered into the running turn (steering): the desktop row's read mark.
127
+ ...(extra?.steered === true ? { steered: true } : {}),
126
128
  // Byte-free attachment metadata (name/mime/size) from the queue entry —
127
129
  // lets the desktop transcript render image chips without ever carrying
128
130
  // base64 payloads through snapshots.
@@ -6,8 +6,10 @@
6
6
  * Providers report a call's exact output tokens only when it ends (the usage
7
7
  * delta), so mid-stream the count is the runtime's own token estimate of the
8
8
  * text and reasoning streamed so far (token-estimate.mjs, measured against a
9
- * real tokenizer at +6.5% aggregate bias). The clock starts at the call's first
10
- * visible text/reasoning token, so queueing and time-to-first-token stay out.
9
+ * real tokenizer at +6.5% aggregate bias). The clock starts when the provider
10
+ * request starts (markLiveCallRequestStart), the wait for the first token
11
+ * included, the same span as the persisted speed and the usage ledger; with no
12
+ * request-start mark it starts at the first visible text/reasoning token.
11
13
  *
12
14
  * Published at most every LIVE_CALL_PUBLISH_MS with its own sample time, so the
13
15
  * reader divides tokens by (sample time - start) and a pause between samples
@@ -25,11 +27,16 @@ function publish({ getState, set }, liveCall) {
25
27
  if (spinner) set({ spinner: { ...spinner, liveCall } });
26
28
  }
27
29
 
30
+ /** The provider request of the next call started: its clock starts here. */
31
+ export function markLiveCallRequestStart(stream, now = Date.now()) {
32
+ stream.liveCallRequestAt = now;
33
+ }
34
+
28
35
  /** A text or reasoning chunk of the streaming call arrived. */
29
36
  export function noteLiveCallText(stream, chunk, now = Date.now()) {
30
37
  const text = String(chunk ?? '');
31
38
  if (!text || stream.liveCallWithdrawn) return;
32
- const call = stream.liveCall || { startedAt: now, text: '', publishedAt: 0 };
39
+ const call = stream.liveCall || { startedAt: stream.liveCallRequestAt ?? now, text: '', publishedAt: 0 };
33
40
  stream.liveCall = call;
34
41
  call.text += text;
35
42
  if (call.publishedAt && now - call.publishedAt < LIVE_CALL_PUBLISH_MS) return;
@@ -52,5 +59,6 @@ export function endLiveCall(stream) {
52
59
 
53
60
  export function startNextLiveCall(stream) {
54
61
  stream.liveCallWithdrawn = false;
62
+ stream.liveCallRequestAt = null;
55
63
  endLiveCall(stream);
56
64
  }
@@ -54,7 +54,13 @@ import {
54
54
  startLiveTail,
55
55
  } from './turn-tool-cards.mjs';
56
56
  import { startQuietWatch } from './turn-quiet.mjs';
57
- import { endLiveCall, noteLiveCallText, startNextLiveCall, withdrawLiveCall } from './turn-live-call.mjs';
57
+ import {
58
+ endLiveCall,
59
+ markLiveCallRequestStart,
60
+ noteLiveCallText,
61
+ startNextLiveCall,
62
+ withdrawLiveCall,
63
+ } from './turn-live-call.mjs';
58
64
 
59
65
  // A usage limit is exactly a spent quota, a plan-gated feature or a billing
60
66
  // refusal; overloads, rate limits and server faults recover by waiting.
@@ -204,8 +210,9 @@ function steeringItemExtras(steeringMeta) {
204
210
  ? { messageId: steeringMeta.messageId }
205
211
  : {}),
206
212
  // Queue mode + execution provenance: a task notification is rendered as
207
- // a tool card, never as an injected user bubble.
208
- ...(steeringMeta?.mode === 'task-notification' ? { mode: 'task-notification' } : {}),
213
+ // a tool card, never as an injected user bubble. Anything else is a
214
+ // message the model read inside its running turn (the row's read mark).
215
+ ...(steeringMeta?.mode === 'task-notification' ? { mode: 'task-notification' } : { steered: true }),
209
216
  ...(steeringMeta?.execution && typeof steeringMeta.execution === 'object'
210
217
  ? { execution: steeringMeta.execution }
211
218
  : {}),
@@ -427,6 +434,9 @@ export function toolHandlers(ctx) {
427
434
  export function streamHandlers(ctx) {
428
435
  const { isCurrentTurn, stream } = ctx;
429
436
  return {
437
+ onProviderSendStarted: () => {
438
+ if (isCurrentTurn()) markLiveCallRequestStart(stream);
439
+ },
430
440
  onStageChange: async (stage, detail = null) => {
431
441
  if (!isCurrentTurn()) return;
432
442
  // A runtime notice (e.g. a tool the provider left out of the request)