mixdog 1.3.4 → 1.3.5

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 (197) hide show
  1. package/LICENSES/oh-my-pi-MIT.txt +23 -0
  2. package/NOTICE.md +15 -0
  3. package/README.md +11 -0
  4. package/package.json +2 -1
  5. package/scripts/embed-google-oauth-client.mjs +49 -0
  6. package/scripts/lib/test-spawn-ledger.mjs +46 -1
  7. package/scripts/test.mjs +5 -0
  8. package/src/defaults/shipped-history.json +8 -0
  9. package/src/defaults/skills/browser-use/SKILL.md +21 -9
  10. package/src/defaults/skills/setup/SKILL.md +7 -2
  11. package/src/defaults/skills/setup/references/actions.md +82 -27
  12. package/src/defaults/skills/setup/references/surfaces.md +67 -34
  13. package/src/defaults/skills/setup-guide/SKILL.md +3 -3
  14. package/src/lib/keychain-cjs.cjs +359 -29
  15. package/src/rules/agent-mode/AGENT_MODE.md +10 -2
  16. package/src/rules/agent-mode/MIXY.md +12 -0
  17. package/src/runtime/agent/orchestrator/agent-runtime/heartbeat-completion.mjs +193 -0
  18. package/src/runtime/agent/orchestrator/config.mjs +18 -0
  19. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +2 -2
  20. package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
  21. package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +31 -2
  22. package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +16 -3
  23. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +42 -7
  24. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +21 -2
  25. package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +23 -0
  26. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +11 -1
  27. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +68 -23
  28. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +22 -1
  29. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +18 -5
  30. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  31. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -3
  32. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +52 -8
  33. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +10 -1
  34. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +22 -6
  35. package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +15 -4
  36. package/src/runtime/agent/orchestrator/providers/antigravity-stream.mjs +17 -2
  37. package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +19 -2
  38. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
  39. package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +10 -4
  40. package/src/runtime/agent/orchestrator/providers/cursor-wire-protobuf.mjs +10 -217
  41. package/src/runtime/agent/orchestrator/providers/cursor.mjs +7 -0
  42. package/src/runtime/agent/orchestrator/providers/devin-auth.mjs +409 -0
  43. package/src/runtime/agent/orchestrator/providers/devin-wire-families.mjs +187 -0
  44. package/src/runtime/agent/orchestrator/providers/devin-wire-identity.mjs +342 -0
  45. package/src/runtime/agent/orchestrator/providers/devin-wire-models.mjs +541 -0
  46. package/src/runtime/agent/orchestrator/providers/devin-wire-protobuf.mjs +305 -0
  47. package/src/runtime/agent/orchestrator/providers/devin-wire-request.mjs +558 -0
  48. package/src/runtime/agent/orchestrator/providers/devin-wire-schema.mjs +880 -0
  49. package/src/runtime/agent/orchestrator/providers/devin-wire-session.mjs +190 -0
  50. package/src/runtime/agent/orchestrator/providers/devin-wire-tool-args.mjs +351 -0
  51. package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +560 -0
  52. package/src/runtime/agent/orchestrator/providers/devin.mjs +491 -0
  53. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +3 -0
  54. package/src/runtime/agent/orchestrator/providers/gemini-output-limit.mjs +46 -0
  55. package/src/runtime/agent/orchestrator/providers/gemini-reasoning-relay.mjs +30 -0
  56. package/src/runtime/agent/orchestrator/providers/gemini-request-body.mjs +7 -11
  57. package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +61 -22
  58. package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +1 -0
  59. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +71 -4
  60. package/src/runtime/agent/orchestrator/providers/gemini.mjs +25 -2
  61. package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
  62. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +96 -5
  63. package/src/runtime/agent/orchestrator/providers/lib/tail-reminder-split.mjs +23 -5
  64. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +17 -0
  65. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +39 -3
  66. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +3 -1
  67. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +2 -0
  68. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +6 -1
  69. package/src/runtime/agent/orchestrator/providers/opencode-go-gemini.mjs +1 -0
  70. package/src/runtime/agent/orchestrator/providers/provider-availability.mjs +19 -0
  71. package/src/runtime/agent/orchestrator/providers/registry.mjs +17 -3
  72. package/src/runtime/agent/orchestrator/providers/wire-protobuf-codec.mjs +368 -0
  73. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +2 -0
  74. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +1 -1
  75. package/src/runtime/agent/orchestrator/runtime-core/reaction-tool-defs.mjs +133 -0
  76. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
  77. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +23 -5
  78. package/src/runtime/agent/orchestrator/runtime-core/tool-profile.mjs +35 -10
  79. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +57 -10
  80. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +9 -6
  81. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +1 -1
  82. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +18 -0
  83. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +0 -3
  84. package/src/runtime/agent/orchestrator/session/agent-room.mjs +10 -4
  85. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +15 -3
  86. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +5 -0
  87. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +36 -4
  88. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +6 -0
  89. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +15 -3
  90. package/src/runtime/agent/orchestrator/session/loop/assistant-commit.mjs +8 -0
  91. package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +8 -1
  92. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +4 -1
  93. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +50 -2
  94. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +3 -0
  95. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +11 -2
  96. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +11 -2
  97. package/src/runtime/agent/orchestrator/session/loop/turn-limits.mjs +60 -0
  98. package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +2 -0
  99. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +14 -0
  100. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -1
  101. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +13 -0
  102. package/src/runtime/agent/orchestrator/session/message-ids.mjs +5 -3
  103. package/src/runtime/agent/orchestrator/session/message-reactions.mjs +89 -0
  104. package/src/runtime/agent/orchestrator/session/message-reactions.test-support.mjs +75 -0
  105. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +0 -2
  106. package/src/runtime/agent/orchestrator/session/routine-result.mjs +11 -2
  107. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +20 -2
  108. package/src/runtime/agent/orchestrator/session/store-agent-worker-header-cache.mjs +85 -0
  109. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +125 -12
  110. package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +11 -5
  111. package/src/runtime/agent/orchestrator/session/tool-batch/state.mjs +4 -0
  112. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +5 -0
  113. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +7 -1
  114. package/src/runtime/channels/lib/schedule-errors.mjs +51 -0
  115. package/src/runtime/channels/lib/scheduler.mjs +109 -14
  116. package/src/runtime/channels/lib/schedules-db.mjs +49 -0
  117. package/src/runtime/channels/lib/tool-dispatch.mjs +13 -0
  118. package/src/runtime/channels/lib/worker-main.mjs +9 -0
  119. package/src/runtime/office/com/com-adapter.mjs +17 -1
  120. package/src/runtime/office/com/office-com-host.ps1 +51 -1
  121. package/src/runtime/office/pdf/cfb-fixture.mjs +328 -0
  122. package/src/runtime/office/pdf/cfb-protection.mjs +311 -0
  123. package/src/runtime/office/pdf/document-preview.mjs +326 -8
  124. package/src/runtime/office/pdf/ooxml-crypto.mjs +469 -0
  125. package/src/runtime/office/pdf/ooxml-key-worker.mjs +14 -0
  126. package/src/runtime/office/portable/portable-soffice.mjs +45 -5
  127. package/src/runtime/shared/bot-hops.mjs +62 -2
  128. package/src/runtime/shared/config.mjs +49 -22
  129. package/src/runtime/shared/connector-catalog.d.mts +4 -2
  130. package/src/runtime/shared/connector-catalog.mjs +16 -8
  131. package/src/runtime/shared/developer-options.mjs +32 -1
  132. package/src/runtime/shared/google-oauth-client.generated.json +4 -0
  133. package/src/runtime/shared/google-oauth-client.mjs +34 -9
  134. package/src/runtime/shared/llm/anthropic-betas.mjs +7 -0
  135. package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +12 -0
  136. package/src/runtime/shared/llm/provider-catalog-registry.mjs +1 -0
  137. package/src/runtime/shared/llm/well-formed.mjs +66 -0
  138. package/src/runtime/shared/provider-accounts.mjs +224 -58
  139. package/src/runtime/shared/provider-api-key.mjs +19 -8
  140. package/src/runtime/shared/routine-trigger.mjs +42 -0
  141. package/src/runtime/shared/tool-execution-contract.mjs +2 -0
  142. package/src/runtime/shared/transcript-metadata.mjs +38 -0
  143. package/src/runtime/shared/user-data-guard.mjs +30 -2
  144. package/src/runtime/shared/worktrees/activity-gate.mjs +12 -5
  145. package/src/session-runtime/agent-home-delivery.mjs +57 -13
  146. package/src/session-runtime/agent-room-delivery.mjs +28 -1
  147. package/src/session-runtime/agent-task-sessions.mjs +292 -79
  148. package/src/session-runtime/boot/begin.mjs +3 -2
  149. package/src/session-runtime/boot/tools.mjs +2 -0
  150. package/src/session-runtime/bot-chain.mjs +32 -0
  151. package/src/session-runtime/bot-status.mjs +7 -4
  152. package/src/session-runtime/channel-config-api.mjs +63 -17
  153. package/src/session-runtime/internal-tool-executor/reaction-tool.mjs +45 -0
  154. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +72 -5
  155. package/src/session-runtime/internal-tool-executor.mjs +4 -0
  156. package/src/session-runtime/mcp-glue/server-catalog.mjs +7 -1
  157. package/src/session-runtime/mixy-heartbeat.mjs +807 -0
  158. package/src/session-runtime/model-route/route-tuning.mjs +10 -6
  159. package/src/session-runtime/owned-automation-run.mjs +33 -12
  160. package/src/session-runtime/provider-auth/login.mjs +7 -0
  161. package/src/session-runtime/provider-models/row-hydration.mjs +13 -2
  162. package/src/session-runtime/routine-home-turn.mjs +33 -6
  163. package/src/session-runtime/schedule-session-run.mjs +7 -3
  164. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +8 -12
  165. package/src/session-runtime/services/channel-admin.mjs +66 -18
  166. package/src/session-runtime/services/channel-worker.mjs +1 -1
  167. package/src/session-runtime/services/provider-admin.mjs +128 -25
  168. package/src/session-runtime/services/schedule-access.mjs +14 -0
  169. package/src/session-runtime/services/usage-dashboard-model.mjs +1 -0
  170. package/src/session-runtime/session-create/session-options.mjs +7 -4
  171. package/src/session-runtime/settings-system-api.mjs +16 -1
  172. package/src/session-runtime/setup-tool/executor.mjs +49 -14
  173. package/src/session-runtime/setup-tool/extended-actions.mjs +29 -3
  174. package/src/session-runtime/setup-tool/settings-contract.mjs +65 -4
  175. package/src/session-runtime/setup-tool/tool-defs.mjs +30 -3
  176. package/src/session-runtime/task-session-route.mjs +99 -0
  177. package/src/session-runtime/tool-policy/policy-stage.mjs +5 -2
  178. package/src/session-runtime/tool-policy/surface-helpers.mjs +2 -3
  179. package/src/session-runtime/tool-surface.mjs +4 -0
  180. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +98 -21
  181. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +6 -1
  182. package/src/session-runtime/workspace/transitions.mjs +10 -2
  183. package/src/standalone/daemon-session-control.mjs +88 -20
  184. package/src/standalone/daemon.mjs +8 -0
  185. package/src/standalone/session-protocol.mjs +5 -0
  186. package/src/standalone/session-service/agent-tree/agent-cancel.mjs +6 -2
  187. package/src/standalone/session-service/session-calls/session-turns.mjs +6 -0
  188. package/src/standalone/session-service.mjs +13 -0
  189. package/src/standalone/session-tool-output.mjs +8 -5
  190. package/src/tui/app/model-options.mjs +2 -0
  191. package/src/tui/dist/index.mjs +24 -2
  192. package/src/tui/session/notice-surface.mjs +17 -1
  193. package/src/tui/session/session-api/integrations.mjs +1 -0
  194. package/src/tui/session/session-api/lifecycle.mjs +8 -0
  195. package/src/tui/session/session-api/settings.mjs +14 -2
  196. package/src/tui/session/turn-stream.mjs +17 -1
  197. package/src/tui/session/turn.mjs +22 -2
@@ -17,10 +17,14 @@ export function createRouteTuning(deps, persist) {
17
17
  * is already the Main model (persistAdoptedModelSettings: tunesNonMainModel),
18
18
  * so the lead preset's model is rewritten with itself and never changes. A
19
19
  * live route without a model normalizes to nothing and writes no preset. */
20
- async function commitRouteTuning(fastCapable, modelMeta) {
21
- persist.saveRouteModelSettings(getRoute(), fastCapable);
22
- const leadRoute = persist.persistAdoptedModelSettings(getRoute());
23
- if (leadRoute) setRouteState(resolveRoute(getConfig(), { model: workflowPresetId('lead') }));
20
+ async function commitRouteTuning(fastCapable, modelMeta, { sessionOnly = false } = {}) {
21
+ // sessionOnly: the live route and session change; the saved model
22
+ // settings and the config stay as the user left them.
23
+ if (!sessionOnly) {
24
+ persist.saveRouteModelSettings(getRoute(), fastCapable);
25
+ const leadRoute = persist.persistAdoptedModelSettings(getRoute());
26
+ if (leadRoute) setRouteState(resolveRoute(getConfig(), { model: workflowPresetId('lead') }));
27
+ }
24
28
  await refreshRouteEffort(modelMeta);
25
29
  persist.applySessionTuning();
26
30
  }
@@ -65,7 +69,7 @@ export function createRouteTuning(deps, persist) {
65
69
  async toggleFast() {
66
70
  return await setFast(!(getRoute().fast === true));
67
71
  },
68
- async setEffort(value) {
72
+ async setEffort(value, { sessionOnly = false } = {}) {
69
73
  const normalized = normalizeEffortInput(value);
70
74
  // An explicit level is the user's choice for this route: Auto effort
71
75
  // would replace it on the next turn (its base is fixed), so the pick
@@ -73,7 +77,7 @@ export function createRouteTuning(deps, persist) {
73
77
  setRouteState({ ...getRoute(), effort: normalized, ...(normalized ? { autoEffort: false } : {}) });
74
78
  const modelMeta = await lookupModelMeta(getRoute().provider, getRoute().model);
75
79
  const fastCapable = selectionFastCapable(getRoute().provider, modelMeta, normalized, getRoute().modelParameters);
76
- await commitRouteTuning(fastCapable, modelMeta);
80
+ await commitRouteTuning(fastCapable, modelMeta, { sessionOnly });
77
81
  return getRoute();
78
82
  },
79
83
  };
@@ -27,6 +27,21 @@ const defaultLog = (line) => process.stderr.write(`mixdog routines: ${line}\n`);
27
27
  // received webhooks and the app's "Run now" all come through here.
28
28
  const activeRuns = new Set();
29
29
 
30
+ /**
31
+ * Reserve `kind:name` against runs in this process for a rename. Null when a
32
+ * run is in flight; otherwise a release function. The check and the reservation
33
+ * are one synchronous step, and a run that finds the reservation is skipped as
34
+ * an overlap (runOwnedAutomation), so none can start under the name meanwhile.
35
+ * "Run now" takes the same reservation to hold the name from its read of the
36
+ * row through its outcome stamps (runOwnedAutomation `reserved`).
37
+ */
38
+ export function beginAutomationRename(kind, name, active = activeRuns) {
39
+ const key = `${kind}:${name}`;
40
+ if (active.has(key)) return null;
41
+ active.add(key);
42
+ return () => active.delete(key);
43
+ }
44
+
30
45
  // The PG record of each kind, imported on first use.
31
46
  const RECORDS = {
32
47
  schedule: () =>
@@ -103,7 +118,8 @@ async function queueResult(row, item, entry, { deliver, recordDelivery, onDelive
103
118
  * result }` (`signal` is the caller's cancellation, if any). Resolves the
104
119
  * outcome, or `{ skipped: true, reason: 'owner' | 'overlap' | 'backoff' | 'paused' }`
105
120
  * (an owner skip also names `ownerProblem` and its `message`); a failed run
106
- * rejects.
121
+ * rejects. `reserved`: the caller already holds `kind:name` (beginAutomationRename)
122
+ * and releases it itself, so this run neither checks for nor releases it.
107
123
  */
108
124
  export async function runOwnedAutomation(kind, row, run, deps = {}) {
109
125
  if (!row?.name) throw new Error(`${kind} run: a ${kind} row is required`);
@@ -112,6 +128,7 @@ export async function runOwnedAutomation(kind, row, run, deps = {}) {
112
128
  active = activeRuns,
113
129
  limits = ROUTINE_LIMITS,
114
130
  attended = false,
131
+ reserved = false,
115
132
  signal = null,
116
133
  now = Date.now,
117
134
  ownerProblem = routineOwnerProblem,
@@ -125,17 +142,21 @@ export async function runOwnedAutomation(kind, row, run, deps = {}) {
125
142
  .catch((error) => log(`${kind} "${name}": skip record failed: ${errorText(error)}`));
126
143
  return { name, skipped: true, reason, sessionId: null, result: '', ...extra };
127
144
  };
128
- // A routine runs only in its own bot's thread: without that bot it waits.
129
- const problem = await Promise.resolve()
130
- .then(() => ownerProblem(row.ownerAgentId))
131
- .catch(() => '');
132
- if (problem) {
133
- log(`${kind} "${name}": paused, ${ROUTINE_OWNER_PROBLEMS[problem]}`);
134
- return skip('owner', { ownerProblem: problem, message: ROUTINE_OWNER_PROBLEMS[problem] });
135
- }
136
- if (active.has(key)) return skip('overlap');
137
- active.add(key);
145
+ // The name is reserved before the owner check, so a rename can neither
146
+ // complete while it waits nor leave this run to start under the old name.
147
+ const overlap = !reserved && active.has(key);
148
+ const holds = !reserved && !overlap;
149
+ if (holds) active.add(key);
138
150
  try {
151
+ // A routine runs only in its own bot's thread: without that bot it waits.
152
+ const problem = await Promise.resolve()
153
+ .then(() => ownerProblem(row.ownerAgentId))
154
+ .catch(() => '');
155
+ if (problem) {
156
+ log(`${kind} "${name}": paused, ${ROUTINE_OWNER_PROBLEMS[problem]}`);
157
+ return await skip('owner', { ownerProblem: problem, message: ROUTINE_OWNER_PROBLEMS[problem] });
158
+ }
159
+ if (overlap) return await skip('overlap');
139
160
  if (!attended) {
140
161
  const record = await withRecord(kind, deps);
141
162
  const state = await Promise.resolve()
@@ -156,7 +177,7 @@ export async function runOwnedAutomation(kind, row, run, deps = {}) {
156
177
  }
157
178
  return await run({ signal });
158
179
  } finally {
159
- active.delete(key);
180
+ if (holds) active.delete(key);
160
181
  }
161
182
  }
162
183
 
@@ -8,6 +8,7 @@ import {
8
8
  saveOpenAIUsageSessionKey,
9
9
  saveProviderApiKey,
10
10
  saveCustomProvider,
11
+ saveCustomProviderKey,
11
12
  removeCustomProvider,
12
13
  updateCustomProviderModel,
13
14
  saveModelPreferences,
@@ -76,6 +77,12 @@ export function createLoginApi({ cfgMod, awaitKeychainPrewarm, reloadFullConfig,
76
77
  refresh.afterCredentialChange();
77
78
  return result;
78
79
  },
80
+ async saveCustomProviderKey(providerId, secret) {
81
+ await awaitKeychainPrewarm();
82
+ const result = await saveCustomProviderKey(cfgMod, providerId, secret);
83
+ refresh.afterCredentialChange();
84
+ return result;
85
+ },
79
86
  async updateCustomProviderModel(providerId, modelId, patch) {
80
87
  const result = await updateCustomProviderModel(cfgMod, providerId, modelId, patch);
81
88
  refresh.afterCredentialChange();
@@ -14,6 +14,7 @@ import {
14
14
  effortConfigurationMode,
15
15
  } from '../../runtime/agent/orchestrator/providers/effort-configuration.mjs';
16
16
  import { getProvider } from '../../runtime/agent/orchestrator/providers/registry.mjs';
17
+ import { providerAvailable } from '../../runtime/agent/orchestrator/providers/provider-availability.mjs';
17
18
  import {
18
19
  DAYBREAK_PARAMETER_OPTION,
19
20
  DAYBREAK_PROVIDER,
@@ -96,7 +97,17 @@ export function createRowHydration({
96
97
  hydrateProviderModelRow: (row) => hydrateProviderModelRow(config(), row),
97
98
  sortProviderModels,
98
99
  providerModelCacheRow,
99
- providerModelsFromCacheRows: (rows) =>
100
- sortProviderModels((rows || []).map((row) => hydrateProviderModelRow(config(), row))),
100
+ // Cached rows outlive a Developer option switch, so availability is
101
+ // applied at read time, once per provider per call.
102
+ providerModelsFromCacheRows: (rows) => {
103
+ const availability = new Map();
104
+ const available = (provider) => {
105
+ if (!availability.has(provider)) availability.set(provider, providerAvailable(provider));
106
+ return availability.get(provider);
107
+ };
108
+ return sortProviderModels(
109
+ (rows || []).filter((row) => available(row.provider)).map((row) => hydrateProviderModelRow(config(), row))
110
+ );
111
+ },
101
112
  };
102
113
  }
@@ -14,14 +14,21 @@
14
14
  * (outbound actions are allowed within the bot's permissions; payments and
15
15
  * permanent deletes are refused, human-gate/run-policy.mjs). Resolves with the
16
16
  * reply once the turn ends; rejects when the turn fails, is cancelled, or the
17
- * caller's `signal` aborts (which also aborts the turn once it runs).
17
+ * caller's `signal` aborts (which also aborts the turn once it runs). A
18
+ * heartbeat turn past its wall-clock cap is aborted and resolves with no reply
19
+ * (`timedOut`).
18
20
  */
19
21
  import { randomUUID } from 'node:crypto';
20
22
  import { agentHomeSessionId } from '../runtime/agent/orchestrator/session/agent-home.mjs';
21
23
  import { routineHumanNeededText } from '../runtime/agent/orchestrator/session/routine-result.mjs';
22
24
  import { automationPromptContent } from '../runtime/shared/automation-attachments.mjs';
23
25
  import { ROUTINE_LIMITS } from '../runtime/shared/routine-limits.mjs';
24
- import { normalizeRoutineTrigger, routineTriggerHeader } from '../runtime/shared/routine-trigger.mjs';
26
+ import {
27
+ HEARTBEAT_TURN_LIMITS,
28
+ isHeartbeatSession,
29
+ normalizeRoutineTrigger,
30
+ routineTriggerHeader,
31
+ } from '../runtime/shared/routine-trigger.mjs';
25
32
  import { agentHomeLiveSurface, scheduleOwnerAgentId } from './agent-home-delivery.mjs';
26
33
  import { markUnattendedRun, unattendedRunOf } from './held-requests.mjs';
27
34
 
@@ -60,12 +67,18 @@ export async function runRoutineTurn(
60
67
  cwd = null,
61
68
  event = '',
62
69
  signal = null,
70
+ // A session the caller created for this turn and the turn's opening line
71
+ // (the heartbeat's hidden session and header); default the owner's home
72
+ // thread and the routine trigger header.
73
+ sessionId = null,
74
+ header = null,
63
75
  },
64
76
  {
65
77
  surface = agentHomeLiveSurface(),
66
78
  loadSession = defaultLoadSession,
67
79
  limit = ROUTINE_LIMITS.homeQueueLimit,
68
80
  log = (line) => process.stderr.write(`mixdog routines: ${line}\n`),
81
+ timers = globalThis,
69
82
  } = {}
70
83
  ) {
71
84
  const body = String(prompt || '').trim();
@@ -73,8 +86,8 @@ export async function runRoutineTurn(
73
86
  if (signal?.aborted) throw abortError(signal);
74
87
  if (typeof surface?.submit !== 'function') throw new Error('routine run: the session service is not running');
75
88
  const agentId = scheduleOwnerAgentId({ ownerAgentId });
76
- const homeId = agentHomeSessionId(agentId);
77
- const home = await loadSession(homeId);
89
+ const homeId = sessionId || agentHomeSessionId(agentId);
90
+ const home = sessionId ? true : await loadSession(homeId);
78
91
  if (!home || home.closed === true) {
79
92
  throw new Error(`the bot "${agentId}" has no chat yet: open it once in the app`);
80
93
  }
@@ -82,14 +95,15 @@ export async function runRoutineTurn(
82
95
  if (queued >= limit) throw new Error(`${limit} routine runs already wait in this bot's chat`);
83
96
  waiting.set(homeId, queued + 1);
84
97
  const trigger = normalizeRoutineTrigger({ kind, name, event });
85
- const header = routineTriggerHeader({ kind, name, project: cwd ? String(cwd) : '' });
86
- const content = automationPromptContent(`${header}\n\n${body}`, attachments);
98
+ const opening = header || routineTriggerHeader({ kind, name, project: cwd ? String(cwd) : '' });
99
+ const content = automationPromptContent(`${opening}\n\n${body}`, attachments);
87
100
  return new Promise((resolve, reject) => {
88
101
  let started = false;
89
102
  let aborted = false;
90
103
  let done = false;
91
104
  let unmark = () => {};
92
105
  let released = false;
106
+ let cutoff = null;
93
107
  const release = () => {
94
108
  if (released) return;
95
109
  released = true;
@@ -101,6 +115,7 @@ export async function runRoutineTurn(
101
115
  const finish = (fn, value) => {
102
116
  if (done) return;
103
117
  done = true;
118
+ if (cutoff) timers.clearTimeout(cutoff);
104
119
  fn(value);
105
120
  };
106
121
  const abortTurn = () =>
@@ -113,6 +128,18 @@ export async function runRoutineTurn(
113
128
  finish(reject, abortError(signal));
114
129
  }
115
130
  signal?.addEventListener?.('abort', onAbort, { once: true });
131
+ // Mixy's heartbeat turn (by its in-process mark) is cut off at its
132
+ // wall-clock cap: the turn is aborted and ends cleanly with no reply.
133
+ if (sessionId && isHeartbeatSession(homeId)) {
134
+ const ms = HEARTBEAT_TURN_LIMITS.wallClockMs;
135
+ cutoff = timers.setTimeout(() => {
136
+ log(`${kind} "${name}" in ${homeId} passed ${ms / 1000}s: ended without a reply`);
137
+ aborted = true;
138
+ if (started) void abortTurn();
139
+ finish(resolve, { sessionId: homeId, result: '', timedOut: true });
140
+ }, ms);
141
+ cutoff?.unref?.();
142
+ }
116
143
  const options = {
117
144
  id: `routine_${randomUUID()}`,
118
145
  mode: 'routine',
@@ -11,9 +11,13 @@ import { runRoutineTurn } from './routine-home-turn.mjs';
11
11
  /**
12
12
  * One fire of `schedule`. `prompt` overrides schedule.prompt when the caller
13
13
  * (channels worker) already resolved the body; `attended` marks a person's
14
- * "Run now" (no failure backoff).
14
+ * "Run now" (no failure backoff); `reserved` that the caller holds the name
15
+ * (owned-automation-run.mjs beginAutomationRename).
15
16
  */
16
- export async function runScheduleSession(schedule, { attended = false, prompt = null, signal = null } = {}) {
17
+ export async function runScheduleSession(
18
+ schedule,
19
+ { attended = false, prompt = null, signal = null, reserved = false } = {}
20
+ ) {
17
21
  const body = String(prompt ?? schedule.prompt ?? '').trim();
18
22
  return runOwnedSchedule(
19
23
  schedule,
@@ -27,7 +31,7 @@ export async function runScheduleSession(schedule, { attended = false, prompt =
27
31
  cwd: schedule.cwd || null,
28
32
  signal: runSignal,
29
33
  }),
30
- { attended, signal }
34
+ { attended, signal, reserved }
31
35
  );
32
36
  }
33
37
 
@@ -20,9 +20,8 @@ import { resolveAgentSpawnPreset } from '../spawn-preset.mjs';
20
20
  import { AGENT_OWNER } from '../../../../runtime/agent/orchestrator/agent-owner.mjs';
21
21
  import { workspaceCwdLock } from '../../../workspace/admission-gate.mjs';
22
22
  import { isContactAgentId } from '../../../../runtime/agent/orchestrator/runtime-core/workflow-ids.mjs';
23
- import { AGENT_MODE, resolveSessionMode } from '../../../../runtime/agent/orchestrator/session/agent-mode.mjs';
24
- import { MIXY_AGENT_ID } from '../../../../runtime/agent/orchestrator/session/agent-home.mjs';
25
- import { mixyDisplayName } from '../../../../runtime/agent/orchestrator/session/mixy-name.mjs';
23
+ import { assertBotChainTarget } from '../../../../runtime/shared/bot-hops.mjs';
24
+ import { actingAgentOf, delegationChainOf } from '../../../bot-chain.mjs';
26
25
 
27
26
  export function createSpawnPlanner({
28
27
  mgr = null,
@@ -50,17 +49,14 @@ export function createSpawnPlanner({
50
49
  if (isAgentDisabled(config, agent)) {
51
50
  throw new Error(`agent spawn: agent "${agent}" is turned off`);
52
51
  }
53
- // Only Mixy hands work to a contact agent; any other agent-mode Lead
54
- // delegates to sub-agents alone (its catalog lists no contact either).
52
+ // Any agent-mode Lead (Mixy or a bot) may hand work to a contact agent,
53
+ // never to one already in its delegation chain (itself included): the
54
+ // session's ancestry and the bot chain its turn was handed (bot-chain.mjs).
55
55
  if (isContactAgentId(agent, readAgentFrontmatterKind(agent, dataDir, STANDALONE_SOURCE_ROOT))) {
56
56
  const callerId = clean(context?.callerSessionId || context?.sessionId);
57
- const caller = (callerId && mgr?.getSession?.(callerId)) || null;
58
- const { mode, agentId } = resolveSessionMode({ session: caller, sessionId: callerId });
59
- if (mode === AGENT_MODE && agentId !== MIXY_AGENT_ID) {
60
- throw new Error(
61
- `agent spawn: only ${mixyDisplayName()} (the representative agent) can hand work to the contact agent "${agent}"; delegate to a sub-agent instead`
62
- );
63
- }
57
+ const getSession = (id) => mgr?.getSession?.(id) || null;
58
+ const chain = delegationChainOf(callerId, getSession);
59
+ assertBotChainTarget(agent, { chain, self: actingAgentOf(getSession(callerId), callerId) }, 'agent spawn');
64
60
  }
65
61
  const agentPermission = readAgentFrontmatterPermission(agent, dataDir, STANDALONE_SOURCE_ROOT);
66
62
  const agentPerm = normalizeAgentPermission(agentPermission) || null;
@@ -11,6 +11,7 @@ import {
11
11
  listSchedules as dbListSchedules,
12
12
  getSchedule as dbGetSchedule,
13
13
  upsertSchedule,
14
+ renameSchedule as dbRenameSchedule,
14
15
  deleteSchedule as dbDeleteSchedule,
15
16
  setEnabled as dbSetEnabled,
16
17
  } from '../../runtime/channels/lib/schedules-db.mjs';
@@ -26,6 +27,7 @@ import { normalizeAutomationAttachments } from '../../runtime/shared/automation-
26
27
  import { assertRoutineOwner, routineOwnerProblem } from '../routine-owner.mjs';
27
28
  import { hasEnabledAutomation } from '../../runtime/shared/automation-presence.mjs';
28
29
  import { resolveScheduleTimezone, validateScheduleCron } from '../../runtime/shared/schedule-time.mjs';
30
+ import { scheduleNameTakenError, scheduleRenameRunningError } from '../../runtime/channels/lib/schedule-errors.mjs';
29
31
  import { pgClusterExists } from '../../runtime/memory/lib/pg/adapter.mjs';
30
32
  import { resolvePluginData } from '../../runtime/shared/plugin-paths.mjs';
31
33
  import { readHookPublicBase } from '../../runtime/channels/lib/webhook/relay-tunnel.mjs';
@@ -270,26 +272,62 @@ export async function listSchedules() {
270
272
  return Promise.all(rows.map(async (row) => scheduleToDisplay(row, await problemOf(row.ownerAgentId))));
271
273
  }
272
274
 
275
+ /**
276
+ * Run `step` (the move of schedule `name`'s row) with `name` reserved against
277
+ * runs in this process (the app's "Run now"). Refused when one is in flight.
278
+ */
279
+ export async function withScheduleRenameReserved(name, step) {
280
+ const { beginAutomationRename } = await import('../owned-automation-run.mjs');
281
+ const release = beginAutomationRename('schedule', name);
282
+ if (!release) throw scheduleRenameRunningError(name);
283
+ try {
284
+ return await step();
285
+ } finally {
286
+ release();
287
+ }
288
+ }
289
+
290
+ // Whether `next` fires when `existing` does: the same cron in the same zone (a
291
+ // stored row without a zone runs in the default one, as the scheduler arms
292
+ // it), or the same instant, whatever its spelling.
293
+ function sameTiming(existing, next) {
294
+ const instant = (value) => (value ? new Date(value).getTime() : null);
295
+ if ((existing.whenCron || null) !== next.whenCron) return false;
296
+ if (next.whenCron && resolveScheduleTimezone(existing.timezone) !== next.timezone) return false;
297
+ return instant(existing.whenAt) === instant(next.whenAt);
298
+ }
299
+
300
+ // The rename when no channels worker owns the scheduler: this process's runs
301
+ // are the only ones there are.
302
+ export const renameScheduleHere = (oldName, def, opts) =>
303
+ withScheduleRenameReserved(oldName, () => dbRenameSchedule(oldName, def, opts));
304
+
273
305
  // Register or update a schedule in the PG store. Recurring input maps `time`
274
306
  // (+ optional `days`) to a cron; one-shot input maps an `at` datetime; the two
275
307
  // are mutually exclusive (also enforced by the store's when_at/when_cron XOR).
276
308
  // Every schedule runs as a turn in its owner bot's chat; a bot that does not
277
309
  // exist (no representative bot yet, an archived bot) cannot own one.
278
- export async function saveSchedule({
279
- name,
280
- description = '',
281
- time,
282
- at,
283
- timezone,
284
- days,
285
- cwd,
286
- attachments,
287
- enabled,
288
- instructions,
289
- owner,
290
- overwrite = false,
291
- } = {}) {
310
+ export async function saveSchedule(
311
+ {
312
+ name,
313
+ description = '',
314
+ time,
315
+ at,
316
+ timezone,
317
+ days,
318
+ cwd,
319
+ attachments,
320
+ enabled,
321
+ instructions,
322
+ owner,
323
+ overwrite = false,
324
+ newName,
325
+ } = {},
326
+ { rename = renameScheduleHere } = {}
327
+ ) {
292
328
  const id = assertScheduleName(name);
329
+ const renameTo = newName != null && String(newName).trim() !== '' ? assertScheduleName(newName) : id;
330
+ const renaming = renameTo !== id;
293
331
  const body = String(instructions || '').trim();
294
332
  if (!body) throw new Error('schedule instructions are required');
295
333
  const hasTime = time != null && String(time).trim() !== '';
@@ -297,16 +335,17 @@ export async function saveSchedule({
297
335
  if (hasTime && hasAt) throw new Error('provide either `time` (recurring) or `at` (one-shot), not both');
298
336
  if (!hasTime && !hasAt) throw new Error('either `time` (recurring cron) or `at` (one-shot datetime) is required');
299
337
  const existing = await dbGetSchedule(id);
338
+ if (renaming && !existing) throw new Error(`schedule "${id}" does not exist`);
300
339
  if (overwrite !== true && existing) {
301
- throw new Error(`schedule "${id}" already exists`);
340
+ throw scheduleNameTakenError(id);
302
341
  }
303
342
  const ownerAgentId = scheduleOwner(owner, existing);
304
343
  await assertRoutineOwner(ownerAgentId);
305
344
  const whenCron = hasTime ? validateScheduleCron(foldDaysIntoCron(normalizeCron(time), days)) : null;
306
345
  const whenAt = hasAt ? parseAtDatetime(at) : null;
307
346
  const scheduleTimezone = hasTime ? resolveScheduleTimezone(timezone) : null;
308
- const saved = await upsertSchedule({
309
- name: id,
347
+ const def = {
348
+ name: renameTo,
310
349
  description: String(description || '').trim(),
311
350
  whenCron,
312
351
  whenAt,
@@ -320,7 +359,16 @@ export async function saveSchedule({
320
359
  enabled: enabled !== false,
321
360
  nextFireAt: whenAt,
322
361
  ownerAgentId,
323
- });
362
+ };
363
+ let saved;
364
+ if (renaming) {
365
+ if (await dbGetSchedule(renameTo)) throw scheduleNameTakenError(renameTo);
366
+ const timingChanged = !sameTiming(existing, { whenCron, whenAt, timezone: scheduleTimezone });
367
+ saved = await rename(id, def, { timingChanged });
368
+ if (!saved) throw new Error(`schedule "${id}" does not exist`);
369
+ } else {
370
+ saved = await upsertSchedule(def);
371
+ }
324
372
  automationProbe.invalidate();
325
373
  return scheduleToDisplay(saved);
326
374
  }
@@ -18,7 +18,7 @@ import { daemonEnv, spawnDaemonCandidate } from './channel-worker/daemon-spawn.m
18
18
 
19
19
  export { pruneStaleChannelClientHeartbeats };
20
20
 
21
- const CHANNEL_TOOLS = new Set(['activate_channel_bridge', 'reload_config']);
21
+ const CHANNEL_TOOLS = new Set(['activate_channel_bridge', 'reload_config', 'rename_schedule']);
22
22
  const CALL_RETRIES = 3;
23
23
 
24
24
  function logLine(path, line) {