mixdog 1.3.3 → 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.
- package/LICENSES/oh-my-pi-MIT.txt +23 -0
- package/NOTICE.md +15 -0
- package/README.md +11 -0
- package/package.json +3 -2
- package/scripts/embed-google-oauth-client.mjs +49 -0
- package/scripts/lib/test-spawn-ledger.mjs +46 -1
- package/scripts/test.mjs +5 -0
- package/src/defaults/shipped-history.json +8 -0
- package/src/defaults/skills/browser-use/SKILL.md +21 -9
- package/src/defaults/skills/setup/SKILL.md +7 -2
- package/src/defaults/skills/setup/references/actions.md +82 -27
- package/src/defaults/skills/setup/references/surfaces.md +67 -34
- package/src/defaults/skills/setup-guide/SKILL.md +3 -3
- package/src/lib/keychain-cjs.cjs +359 -29
- package/src/rules/agent-mode/AGENT_MODE.md +10 -2
- package/src/rules/agent-mode/MIXY.md +12 -0
- package/src/runtime/agent/orchestrator/agent-runtime/heartbeat-completion.mjs +193 -0
- package/src/runtime/agent/orchestrator/config.mjs +18 -0
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +2 -2
- package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
- package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +31 -2
- package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +16 -3
- package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +42 -7
- package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +21 -2
- package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +23 -0
- package/src/runtime/agent/orchestrator/mcp/oauth.mjs +11 -1
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +68 -23
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +22 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +18 -5
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -3
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +52 -8
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +10 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +22 -6
- package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +15 -4
- package/src/runtime/agent/orchestrator/providers/antigravity-stream.mjs +17 -2
- package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +19 -2
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +10 -4
- package/src/runtime/agent/orchestrator/providers/cursor-wire-protobuf.mjs +10 -217
- package/src/runtime/agent/orchestrator/providers/cursor.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/devin-auth.mjs +409 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-families.mjs +187 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-identity.mjs +342 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-models.mjs +541 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-protobuf.mjs +305 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-request.mjs +558 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-schema.mjs +880 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-session.mjs +190 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-tool-args.mjs +351 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +560 -0
- package/src/runtime/agent/orchestrator/providers/devin.mjs +491 -0
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/gemini-output-limit.mjs +46 -0
- package/src/runtime/agent/orchestrator/providers/gemini-reasoning-relay.mjs +30 -0
- package/src/runtime/agent/orchestrator/providers/gemini-request-body.mjs +7 -11
- package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +61 -22
- package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +71 -4
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +25 -2
- package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +96 -5
- package/src/runtime/agent/orchestrator/providers/lib/tail-reminder-split.mjs +23 -5
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +17 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +39 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/opencode-go-gemini.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/provider-availability.mjs +19 -0
- package/src/runtime/agent/orchestrator/providers/registry.mjs +17 -3
- package/src/runtime/agent/orchestrator/providers/wire-protobuf-codec.mjs +368 -0
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +2 -0
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +1 -1
- package/src/runtime/agent/orchestrator/runtime-core/reaction-tool-defs.mjs +133 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog-data.mjs +3 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +23 -5
- package/src/runtime/agent/orchestrator/runtime-core/tool-profile.mjs +35 -10
- package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +57 -10
- package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +9 -6
- package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +18 -0
- package/src/runtime/agent/orchestrator/session/agent-room.d.mts +0 -3
- package/src/runtime/agent/orchestrator/session/agent-room.mjs +10 -4
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +15 -3
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +36 -4
- package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +15 -3
- package/src/runtime/agent/orchestrator/session/loop/assistant-commit.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +8 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +50 -2
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/loop/turn-limits.mjs +60 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/message-ids.mjs +5 -3
- package/src/runtime/agent/orchestrator/session/message-reactions.mjs +89 -0
- package/src/runtime/agent/orchestrator/session/message-reactions.test-support.mjs +75 -0
- package/src/runtime/agent/orchestrator/session/routine-result.d.mts +0 -2
- package/src/runtime/agent/orchestrator/session/routine-result.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +20 -2
- package/src/runtime/agent/orchestrator/session/store-agent-worker-header-cache.mjs +85 -0
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +125 -12
- package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +11 -5
- package/src/runtime/agent/orchestrator/session/tool-batch/state.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +7 -1
- package/src/runtime/channels/lib/schedule-errors.mjs +51 -0
- package/src/runtime/channels/lib/scheduler.mjs +109 -14
- package/src/runtime/channels/lib/schedules-db.mjs +49 -0
- package/src/runtime/channels/lib/tool-dispatch.mjs +13 -0
- package/src/runtime/channels/lib/worker-main.mjs +9 -0
- package/src/runtime/office/com/com-adapter.mjs +17 -1
- package/src/runtime/office/com/office-com-host.ps1 +51 -1
- package/src/runtime/office/pdf/cfb-fixture.mjs +328 -0
- package/src/runtime/office/pdf/cfb-protection.mjs +311 -0
- package/src/runtime/office/pdf/document-preview.mjs +326 -8
- package/src/runtime/office/pdf/ooxml-crypto.mjs +469 -0
- package/src/runtime/office/pdf/ooxml-key-worker.mjs +14 -0
- package/src/runtime/office/portable/portable-soffice.mjs +45 -5
- package/src/runtime/shared/agent-room-host.mjs +1 -1
- package/src/runtime/shared/bot-hops.mjs +62 -2
- package/src/runtime/shared/config.mjs +49 -22
- package/src/runtime/shared/connector-catalog.d.mts +4 -2
- package/src/runtime/shared/connector-catalog.mjs +16 -8
- package/src/runtime/shared/developer-options.mjs +32 -1
- package/src/runtime/shared/google-oauth-client.generated.json +4 -0
- package/src/runtime/shared/google-oauth-client.mjs +34 -9
- package/src/runtime/shared/llm/anthropic-betas.mjs +7 -0
- package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +12 -0
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +1 -0
- package/src/runtime/shared/llm/usage-rollup.mjs +4 -383
- package/src/runtime/shared/llm/well-formed.mjs +66 -0
- package/src/runtime/shared/provider-accounts.mjs +224 -58
- package/src/runtime/shared/provider-api-key.mjs +19 -8
- package/src/runtime/shared/routine-trigger.mjs +42 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -0
- package/src/runtime/shared/transcript-metadata.mjs +38 -0
- package/src/runtime/shared/user-data-guard.mjs +30 -2
- package/src/runtime/shared/worktrees/activity-gate.mjs +12 -5
- package/src/session-runtime/agent-home-delivery.mjs +57 -13
- package/src/session-runtime/agent-room-delivery.mjs +28 -1
- package/src/session-runtime/agent-task-sessions.mjs +292 -79
- package/src/session-runtime/agent-tool-routing.mjs +49 -4
- package/src/session-runtime/boot/begin.mjs +3 -2
- package/src/session-runtime/boot/tools.mjs +4 -0
- package/src/session-runtime/bot-chain.mjs +32 -0
- package/src/session-runtime/bot-status.mjs +7 -4
- package/src/session-runtime/channel-config-api.mjs +63 -17
- package/src/session-runtime/internal-tool-executor/reaction-tool.mjs +45 -0
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +72 -5
- package/src/session-runtime/internal-tool-executor.mjs +4 -0
- package/src/session-runtime/mcp-glue/server-catalog.mjs +7 -1
- package/src/session-runtime/mixy-heartbeat.mjs +807 -0
- package/src/session-runtime/model-route/route-tuning.mjs +10 -6
- package/src/session-runtime/owned-automation-run.mjs +33 -12
- package/src/session-runtime/provider-auth/login.mjs +7 -0
- package/src/session-runtime/provider-models/row-hydration.mjs +13 -2
- package/src/session-runtime/routine-home-turn.mjs +33 -6
- package/src/session-runtime/schedule-session-run.mjs +7 -3
- package/src/session-runtime/services/agent-tool/close-flow.mjs +14 -2
- package/src/session-runtime/services/agent-tool/execute.mjs +6 -1
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +11 -2
- package/src/session-runtime/services/agent-tool/render.mjs +3 -1
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +8 -12
- package/src/session-runtime/services/agent-tool/tool-def.mjs +1 -1
- package/src/session-runtime/services/channel-admin.mjs +66 -18
- package/src/session-runtime/services/channel-worker.mjs +1 -1
- package/src/session-runtime/services/provider-admin.mjs +128 -25
- package/src/session-runtime/services/schedule-access.mjs +14 -0
- package/src/session-runtime/services/usage-dashboard-model.mjs +1 -0
- package/src/session-runtime/session-create/session-options.mjs +7 -4
- package/src/session-runtime/settings-system-api.mjs +16 -1
- package/src/session-runtime/setup-tool/executor.mjs +49 -14
- package/src/session-runtime/setup-tool/extended-actions.mjs +29 -3
- package/src/session-runtime/setup-tool/settings-contract.mjs +65 -4
- package/src/session-runtime/setup-tool/tool-defs.mjs +30 -3
- package/src/session-runtime/task-session-route.mjs +99 -0
- package/src/session-runtime/tool-policy/policy-stage.mjs +5 -2
- package/src/session-runtime/tool-policy/surface-helpers.mjs +2 -3
- package/src/session-runtime/tool-surface.mjs +4 -0
- package/src/session-runtime/usage-stats-api.mjs +1 -1
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +98 -21
- package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +6 -1
- package/src/session-runtime/workspace/transitions.mjs +10 -2
- package/src/standalone/daemon-session-control.mjs +88 -20
- package/src/standalone/daemon.mjs +8 -0
- package/src/standalone/session-protocol.mjs +5 -0
- package/src/standalone/session-service/agent-tree/agent-cancel.mjs +6 -2
- package/src/standalone/session-service/session-calls/session-turns.mjs +6 -0
- package/src/standalone/session-service.mjs +13 -0
- package/src/standalone/session-tool-output.mjs +8 -5
- package/src/tui/app/model-options.mjs +2 -0
- package/src/tui/dist/index.mjs +335 -172
- package/src/tui/session/notice-surface.mjs +17 -1
- package/src/tui/session/session-api/integrations.mjs +1 -0
- package/src/tui/session/session-api/lifecycle.mjs +8 -0
- package/src/tui/session/session-api/settings.mjs +14 -2
- package/src/tui/session/turn-stream.mjs +17 -1
- package/src/tui/session/turn.mjs +22 -2
- package/src/vendor/statusline/src/gateway/claude-current.mjs +0 -58
- package/src/vendor/statusline/src/gateway/route-meta.mjs +1 -361
- package/src/vendor/statusline/src/gateway/session-routes.mjs +0 -55
- package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +0 -285
|
@@ -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
|
-
|
|
22
|
-
|
|
23
|
-
if (
|
|
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
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
if (
|
|
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
|
-
|
|
100
|
-
|
|
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 {
|
|
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
|
|
86
|
-
const content = automationPromptContent(`${
|
|
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(
|
|
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
|
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
listBackgroundTasks,
|
|
6
6
|
taskIdFromArgs,
|
|
7
7
|
} from '../../../runtime/shared/background-tasks.mjs';
|
|
8
|
-
import { agentScope, clean, clearAgentStatuslineRoute } from './helpers.mjs';
|
|
8
|
+
import { agentScope, clean, clearAgentStatuslineRoute, sessionMatchesContext } from './helpers.mjs';
|
|
9
9
|
import { isLeadPoolAgent } from './worker-rows.mjs';
|
|
10
10
|
import { createCloseAll } from './close-flow/close-all.mjs';
|
|
11
11
|
import { closeManagedSession } from '../../lifecycle/shared.mjs';
|
|
@@ -20,6 +20,16 @@ export function createCloseFlow({ mgr, registry, views }) {
|
|
|
20
20
|
// Stale local metadata only: resolveTag found no session in this
|
|
21
21
|
// terminal/scope, so there is no sessionId-safe worker row to delete. Never
|
|
22
22
|
// turn it into a tag-wide persisted-row removal.
|
|
23
|
+
// `tags`/`tagAgents` are shared by every caller on the canonical agent, so a
|
|
24
|
+
// binding alone does not prove the caller owns it: only a live session or a
|
|
25
|
+
// persisted row inside the caller's scope does. Another session's tag must
|
|
26
|
+
// stay untouched.
|
|
27
|
+
function tagBoundByCaller(target, scopedContext) {
|
|
28
|
+
const live = registry.getLiveSession(tags.get(target));
|
|
29
|
+
if (live) return sessionMatchesContext(live, scopedContext);
|
|
30
|
+
return registry.readWorkerRows(scopedContext).some((row) => clean(row.tag) === target);
|
|
31
|
+
}
|
|
32
|
+
|
|
23
33
|
function forgetStaleLocalTag(target, task) {
|
|
24
34
|
registry.unbindTag(target);
|
|
25
35
|
if (task?.taskId) cancelBackgroundTask(task.taskId, CLOSE_REASON);
|
|
@@ -55,7 +65,9 @@ export function createCloseFlow({ mgr, registry, views }) {
|
|
|
55
65
|
}
|
|
56
66
|
const sessionId = registry.resolveTag(target, scopedContext, { scanSessions: registry.wantsSessionScan(args) });
|
|
57
67
|
if (!sessionId) {
|
|
58
|
-
if (!target.startsWith('sess_') && tagAgents.has(target)
|
|
68
|
+
if (!target.startsWith('sess_') && tagAgents.has(target) && tagBoundByCaller(target, scopedContext)) {
|
|
69
|
+
return forgetStaleLocalTag(target, task);
|
|
70
|
+
}
|
|
59
71
|
throw new Error(`agent close: target "${target}" not found`);
|
|
60
72
|
}
|
|
61
73
|
if (isLeadPoolAgent(registry.getLiveSession(sessionId)?.agent)) {
|
|
@@ -67,7 +67,12 @@ export function createAgentExecute({
|
|
|
67
67
|
}
|
|
68
68
|
// Targetless status is the same compact overview as list.
|
|
69
69
|
return renderResult({
|
|
70
|
-
workers
|
|
70
|
+
// Idle workers stay open for follow-ups; the model must see them to close them.
|
|
71
|
+
workers: views.list({
|
|
72
|
+
scanSessions: registry.wantsSessionScan(args),
|
|
73
|
+
context: scopedContext,
|
|
74
|
+
includeIdle: true,
|
|
75
|
+
}),
|
|
71
76
|
jobs: views.listJobs(scopedContext),
|
|
72
77
|
});
|
|
73
78
|
case 'read':
|
|
@@ -25,6 +25,14 @@ export function isActiveWorkerRow(row = {}) {
|
|
|
25
25
|
return !isTerminalWorkerStatus(effective);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
// A finished worker that is still open: it takes follow-ups and can be closed.
|
|
29
|
+
function isOpenIdleRow(row = {}) {
|
|
30
|
+
const status = String(row.status ?? '').trim().toLowerCase();
|
|
31
|
+
const stage = String(row.stage ?? '').trim().toLowerCase();
|
|
32
|
+
if (CANCELLED_WORKER_STATUS.test(stage) || CANCELLED_WORKER_STATUS.test(status)) return false;
|
|
33
|
+
return (status || stage) === 'idle';
|
|
34
|
+
}
|
|
35
|
+
|
|
28
36
|
function rowState(session, runtime) {
|
|
29
37
|
const status = session.closed === true ? 'closed' : session.status || 'idle';
|
|
30
38
|
const stage =
|
|
@@ -78,7 +86,7 @@ function agentRow({ tag, session, runtime, progress, now, indexRow = null }) {
|
|
|
78
86
|
}
|
|
79
87
|
|
|
80
88
|
export function createAgentList({ mgr, refreshTagsFromSessions, agentSessionEntries, progress }) {
|
|
81
|
-
return function list({ scanSessions = false, context = {}, includeTerminal = false } = {}) {
|
|
89
|
+
return function list({ scanSessions = false, context = {}, includeTerminal = false, includeIdle = false } = {}) {
|
|
82
90
|
refreshTagsFromSessions({ scanSessions, context });
|
|
83
91
|
const now = Date.now();
|
|
84
92
|
const rows = [];
|
|
@@ -89,7 +97,8 @@ export function createAgentList({ mgr, refreshTagsFromSessions, agentSessionEntr
|
|
|
89
97
|
// loads the worker's stored session, and a Lead's status pulse (every
|
|
90
98
|
// 2 s) would otherwise read every worker it ever ran, whole transcripts
|
|
91
99
|
// on first sight, synchronously on the daemon loop.
|
|
92
|
-
|
|
100
|
+
const state = rowState(session, runtime);
|
|
101
|
+
if (!includeTerminal && !isActiveWorkerRow(state) && !(includeIdle && isOpenIdleRow(state))) continue;
|
|
93
102
|
rows.push(
|
|
94
103
|
agentRow({
|
|
95
104
|
tag,
|
|
@@ -156,9 +156,11 @@ export function renderResult(value, { includeDiagnostics = false } = {}) {
|
|
|
156
156
|
if (typeof value === 'string') return value;
|
|
157
157
|
if (value && typeof value === 'object') {
|
|
158
158
|
if (Array.isArray(value.workers) || Array.isArray(value.jobs)) return renderRoster(value);
|
|
159
|
+
// close-flow results carry `closed` plus task_id: render the close outcome.
|
|
160
|
+
// close-flow results carry `closed` plus task_id: render the close outcome.
|
|
161
|
+
if (value.closed !== undefined) return renderClosed(value);
|
|
159
162
|
if (value.task_id) return renderTask(value, includeDiagnostics);
|
|
160
163
|
if (value.queued) return renderQueued(value);
|
|
161
|
-
if (value.closed !== undefined) return renderClosed(value);
|
|
162
164
|
if (value.content !== undefined) return renderContent(value);
|
|
163
165
|
}
|
|
164
166
|
return JSON.stringify(value, null, 2);
|
|
@@ -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 {
|
|
24
|
-
import {
|
|
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
|
-
//
|
|
54
|
-
//
|
|
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
|
|
58
|
-
const
|
|
59
|
-
|
|
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;
|
|
@@ -28,7 +28,7 @@ export const AGENT_TOOL = {
|
|
|
28
28
|
agentHidden: false,
|
|
29
29
|
},
|
|
30
30
|
description:
|
|
31
|
-
'Run scoped agent work as background tasks. spawn/send return task_id immediately. Reuse one tag for one scope for its whole lifetime; give distinct tags only to independent scopes. Never mint a new tag because a session expired or was lost — same-tag spawn respawns it with the full brief. Wait for completion notifications; use status/read only for manual recovery.',
|
|
31
|
+
'Run scoped agent work as background tasks. spawn/send return task_id immediately. Reuse one tag for one scope for its whole lifetime; give distinct tags only to independent scopes. Never mint a new tag because a session expired or was lost — same-tag spawn respawns it with the full brief. Wait for completion notifications; use status/read only for manual recovery. Close finished agents yourself.',
|
|
32
32
|
inputSchema: {
|
|
33
33
|
type: 'object',
|
|
34
34
|
properties: {
|
|
@@ -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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
|
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
|
|
309
|
-
name:
|
|
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) {
|