mixdog 1.2.6 → 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.
- package/LICENSES/simple-icons-CC0-1.0.txt +30 -0
- package/NOTICE.md +71 -9
- package/package.json +4 -1
- package/scripts/check-model-recommendations.mjs +89 -0
- package/scripts/gate-local.mjs +7 -0
- package/scripts/lib/isolated-test-env.mjs +16 -0
- package/src/agents/reviewer/AGENT.md +4 -2
- package/src/agents/reviewer/agent.json +1 -1
- package/src/defaults/shipped-history.json +10 -0
- package/src/defaults/skills/browser-use/SKILL.md +5 -0
- package/src/defaults/skills/memory-management/SKILL.md +4 -1
- package/src/defaults/skills/setup/references/actions.md +14 -4
- package/src/defaults/skills/setup-guide/SKILL.md +178 -0
- package/src/defaults/skills/setup-guide/model-recommendations.json +333 -0
- package/src/lib/rules-builder.cjs +55 -6
- package/src/output-styles/common.md +13 -8
- package/src/rules/agent/AGENT.md +4 -0
- package/src/rules/agent-mode/AGENT_MODE.md +24 -0
- package/src/rules/agent-mode/MIXY.md +28 -0
- package/src/rules/agent-mode/ROUTINE.md +6 -0
- package/src/rules/lead/LEAD.md +7 -3
- package/src/rules/shared/70-delivery.md +3 -0
- package/src/runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs +86 -0
- package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +77 -14
- package/src/runtime/agent/orchestrator/context/role-instructions.mjs +241 -14
- package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +4 -2
- package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +216 -0
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +5 -4
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +10 -1
- package/src/runtime/agent/orchestrator/mcp/client.mjs +115 -11
- package/src/runtime/agent/orchestrator/mcp/oauth-account.mjs +97 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +49 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +59 -9
- package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +32 -7
- package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +45 -6
- package/src/runtime/agent/orchestrator/mcp/oauth.mjs +195 -6
- package/src/runtime/agent/orchestrator/mcp/server-connectors.mjs +17 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +7 -2
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +10 -3
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +7 -8
- package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/lib/tool-call-wire-ids.mjs +65 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +8 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +9 -2
- package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +17 -12
- package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +10 -0
- package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +14 -2
- package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +17 -0
- package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +96 -1
- package/src/runtime/agent/orchestrator/runtime-core/context-status-shape.mjs +4 -0
- package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +6 -4
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +14 -0
- package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +1 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-bridge.mjs +12 -6
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +32 -5
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +21 -5
- package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +40 -7
- package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +52 -20
- package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/agent-home.d.mts +4 -0
- package/src/runtime/agent/orchestrator/session/agent-home.mjs +33 -0
- package/src/runtime/agent/orchestrator/session/agent-mode.mjs +62 -0
- package/src/runtime/agent/orchestrator/session/agent-room-supervisor.d.mts +31 -0
- package/src/runtime/agent/orchestrator/session/agent-room-supervisor.mjs +241 -0
- package/src/runtime/agent/orchestrator/session/agent-room.d.mts +166 -0
- package/src/runtime/agent/orchestrator/session/agent-room.mjs +650 -0
- package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +7 -1
- package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +6 -2
- package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +192 -0
- package/src/runtime/agent/orchestrator/session/human-gate/held-message.mjs +95 -0
- package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +221 -0
- package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +195 -0
- package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +167 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +10 -0
- package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +108 -6
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +74 -15
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +9 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +5 -4
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +13 -3
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +18 -8
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-lifecycle-epoch.mjs +0 -8
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +44 -16
- package/src/runtime/agent/orchestrator/session/manager/provider-replay.mjs +50 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +22 -5
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +39 -4
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +33 -24
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +58 -8
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +3 -2
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +7 -1
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/manager/workflow-switch-notice.mjs +58 -0
- package/src/runtime/agent/orchestrator/session/mixy-name.mjs +27 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +32 -5
- package/src/runtime/agent/orchestrator/session/routine-result.d.mts +86 -0
- package/src/runtime/agent/orchestrator/session/routine-result.mjs +329 -0
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/store-record-strict-scan.mjs +28 -9
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +16 -2
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +44 -12
- package/src/runtime/agent/orchestrator/session/store-summary-last-message.mjs +46 -0
- package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +0 -4
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +58 -10
- package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +92 -15
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +113 -5
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +4 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +25 -1
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +6 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +3 -0
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +75 -57
- package/src/runtime/attachments/store.mjs +6 -0
- package/src/runtime/browser-bridge/client.mjs +24 -0
- package/src/runtime/channels/lib/scheduler.mjs +40 -12
- package/src/runtime/channels/lib/schedules-db.mjs +71 -12
- package/src/runtime/channels/lib/webhook/session-dispatch.mjs +36 -16
- package/src/runtime/channels/lib/webhook.mjs +0 -1
- package/src/runtime/channels/lib/webhooks-db.mjs +100 -10
- package/src/runtime/channels/lib/worker-main.mjs +17 -20
- package/src/runtime/computer-bridge/client.mjs +28 -1
- package/src/runtime/connectors/gmail-app-password.mjs +690 -0
- package/src/runtime/connectors/google-mcp.mjs +446 -0
- package/src/runtime/effort-judge/judge-client.mjs +1 -1
- package/src/runtime/effort-judge/model-manifest.json +7 -7
- package/src/runtime/memory/lib/core-memory-access.mjs +69 -0
- package/src/runtime/memory/lib/core-memory-file.mjs +16 -8
- package/src/runtime/memory/lib/core-memory-index.mjs +15 -9
- package/src/runtime/memory/lib/core-memory-management.mjs +10 -4
- package/src/runtime/memory/lib/core-memory-store.mjs +53 -30
- package/src/runtime/memory/lib/core-memory-uniqueness.mjs +10 -6
- package/src/runtime/memory/lib/http-router/session-routes.mjs +3 -2
- package/src/runtime/memory/lib/memory-action-handlers/core-actions.mjs +44 -14
- package/src/runtime/memory/lib/memory-schema/core-and-meta.mjs +11 -0
- package/src/runtime/memory/lib/memory.mjs +7 -1
- package/src/runtime/memory/lib/query-core-recall.mjs +9 -1
- package/src/runtime/memory/lib/query-hybrid-search.mjs +2 -0
- package/src/runtime/memory/lib/tool-call-handler.mjs +2 -0
- package/src/runtime/memory/tool-defs.mjs +5 -0
- package/src/runtime/shared/agent-avatar-sprites.mjs +445 -0
- package/src/runtime/shared/agent-avatar.d.mts +64 -0
- package/src/runtime/shared/agent-avatar.mjs +320 -0
- package/src/runtime/shared/agent-kind.mjs +152 -0
- package/src/runtime/shared/agent-route-config.d.mts +4 -0
- package/src/runtime/shared/agent-route-config.mjs +30 -0
- package/src/runtime/shared/agent-store.mjs +132 -0
- package/src/runtime/shared/atomic-file.mjs +14 -1
- package/src/runtime/shared/bot-split-migration.mjs +457 -0
- package/src/runtime/shared/config-patch.mjs +20 -1
- package/src/runtime/shared/config.mjs +8 -1
- package/src/runtime/shared/connector-accounts.mjs +15 -0
- package/src/runtime/shared/connector-catalog.d.mts +51 -0
- package/src/runtime/shared/connector-catalog.mjs +228 -0
- package/src/runtime/shared/connector-triggers.d.mts +3 -0
- package/src/runtime/shared/connector-triggers.mjs +45 -0
- package/src/runtime/shared/err-text.mjs +3 -1
- package/src/runtime/shared/error-presentation.mjs +91 -6
- package/src/runtime/shared/google-oauth-client.mjs +24 -0
- package/src/runtime/shared/handoff-artifacts.d.mts +20 -0
- package/src/runtime/shared/handoff-artifacts.mjs +61 -0
- package/src/runtime/shared/held-request-host.d.mts +12 -0
- package/src/runtime/shared/held-request-host.mjs +71 -0
- package/src/runtime/shared/llm/provider-model-identities.mjs +6 -1
- package/src/runtime/shared/llm/usage-accounting.mjs +7 -1
- package/src/runtime/shared/model-route-fallback.mjs +79 -7
- package/src/runtime/shared/plain-preview.d.mts +1 -0
- package/src/runtime/shared/plain-preview.mjs +33 -0
- package/src/runtime/shared/routine-limits.d.mts +11 -0
- package/src/runtime/shared/routine-limits.mjs +21 -0
- package/src/runtime/shared/routine-trigger.d.mts +10 -0
- package/src/runtime/shared/routine-trigger.mjs +44 -0
- package/src/runtime/shared/schedule-model-ref.mjs +9 -10
- package/src/runtime/shared/sensitive-path-rules.d.mts +15 -0
- package/src/runtime/shared/sensitive-path-rules.mjs +256 -0
- package/src/runtime/shared/slow-span.mjs +4 -12
- package/src/runtime/shared/statusline/statusline-segments.mjs +13 -1
- package/src/runtime/shared/transcript-metadata.mjs +10 -0
- package/src/runtime/shared/turn-changes.mjs +0 -8
- package/src/runtime/shared/usage-readout-totals.mjs +58 -0
- package/src/runtime/shared/user-data-guard.mjs +119 -38
- package/src/session-runtime/agent-home-delivery.mjs +826 -0
- package/src/session-runtime/agent-profile-changes.mjs +21 -0
- package/src/session-runtime/agent-room-delivery.mjs +133 -0
- package/src/session-runtime/agent-task-sessions.mjs +460 -0
- package/src/session-runtime/boot/apis.mjs +63 -18
- package/src/session-runtime/boot/facade.mjs +5 -8
- package/src/session-runtime/boot/infrastructure.mjs +28 -17
- package/src/session-runtime/boot/providers.mjs +14 -4
- package/src/session-runtime/boot/tools.mjs +3 -0
- package/src/session-runtime/channel-config-api.mjs +53 -5
- package/src/session-runtime/config-lifecycle.mjs +43 -2
- package/src/session-runtime/cwd-plugins/core-memory-context.mjs +36 -20
- package/src/session-runtime/cwd-plugins/plugin-status.mjs +6 -1
- package/src/session-runtime/handoff-attachments.mjs +593 -0
- package/src/session-runtime/held-requests.mjs +228 -0
- package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +121 -0
- package/src/session-runtime/internal-tool-executor/feature-tools.mjs +5 -1
- package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +31 -2
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +56 -3
- package/src/session-runtime/internal-tool-executor.mjs +2 -0
- package/src/session-runtime/lifecycle/session-catalog.mjs +9 -0
- package/src/session-runtime/lifecycle/session-switch/resume.mjs +4 -0
- package/src/session-runtime/lifecycle/shared.mjs +6 -0
- package/src/session-runtime/mcp-glue/server-catalog.mjs +8 -0
- package/src/session-runtime/mcp-glue/server-input.mjs +3 -0
- package/src/session-runtime/mcp-oauth-login.mjs +112 -15
- package/src/session-runtime/model-route/route-persist.mjs +2 -2
- package/src/session-runtime/model-route/route-tuning.mjs +7 -1
- package/src/session-runtime/model-route/set-route.mjs +35 -4
- package/src/session-runtime/native-web-search.mjs +2 -2
- package/src/session-runtime/new-session-config.mjs +6 -1
- package/src/session-runtime/owned-automation-run.mjs +229 -0
- package/src/session-runtime/resource-api.mjs +5 -0
- package/src/session-runtime/resource-mcp-api.mjs +107 -13
- package/src/session-runtime/resource-skills-api.mjs +12 -0
- package/src/session-runtime/room-member-gate.mjs +48 -0
- package/src/session-runtime/route-resolution.mjs +5 -11
- package/src/session-runtime/routine-home-turn.mjs +156 -0
- package/src/session-runtime/routine-owner.mjs +48 -0
- package/src/session-runtime/schedule-session-run.mjs +30 -64
- package/src/session-runtime/services/agent-tool/helpers.mjs +30 -1
- package/src/session-runtime/services/agent-tool/send-flow.mjs +11 -3
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +19 -4
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +2 -0
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +4 -1
- package/src/session-runtime/services/agent-tool/spawn-preset.mjs +5 -8
- package/src/session-runtime/services/agent-tool/tool-def.mjs +7 -0
- package/src/session-runtime/services/channel-admin.mjs +93 -24
- package/src/session-runtime/services/memory-access.mjs +37 -0
- package/src/session-runtime/services/provider-admin.mjs +74 -39
- package/src/session-runtime/services/schedule-access.mjs +91 -0
- package/src/session-runtime/session-create/session-options.mjs +34 -7
- package/src/session-runtime/session-hooks.mjs +16 -8
- package/src/session-runtime/setup-tool/executor.mjs +79 -6
- package/src/session-runtime/setup-tool/extended-actions.mjs +90 -20
- package/src/session-runtime/setup-tool/settings-contract.mjs +14 -6
- package/src/session-runtime/setup-tool/tool-defs.mjs +8 -0
- package/src/session-runtime/skills-api.mjs +16 -1
- package/src/session-runtime/tool-policy-refresh.mjs +249 -29
- package/src/session-runtime/turn/session-ops.mjs +8 -1
- package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +10 -2
- package/src/session-runtime/turn/turn-run/turn-prepare.mjs +14 -0
- package/src/session-runtime/webhook-session-run.mjs +49 -67
- package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +239 -0
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +650 -25
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +67 -7
- package/src/session-runtime/workflow-agents-api/onboarding.mjs +11 -4
- package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +11 -1
- package/src/session-runtime/workflow-agents-api.mjs +4 -1
- package/src/standalone/daemon-agent-control.mjs +1 -8
- package/src/standalone/daemon-session-control.mjs +21 -4
- package/src/standalone/daemon.mjs +31 -5
- package/src/standalone/session-protocol.mjs +27 -0
- package/src/standalone/session-service/agent-tree/agent-turns.mjs +46 -3
- package/src/standalone/session-service/agent-tree.mjs +4 -3
- package/src/standalone/session-service/session-calls/session-turns.mjs +28 -0
- package/src/standalone/session-service.mjs +19 -0
- package/src/tui/app/slash-commands.mjs +20 -0
- package/src/tui/dist/index.mjs +234 -95
- package/src/tui/session/context-state.mjs +6 -1
- package/src/tui/session/notice-surface.mjs +34 -4
- package/src/tui/session/queue-helpers.mjs +2 -1
- package/src/tui/session/session-api/extensions.mjs +4 -0
- package/src/tui/session/session-api/intake/submission.mjs +5 -0
- package/src/tui/session/session-api/integrations.mjs +8 -0
- package/src/tui/session/session-api/lifecycle/session-reset.mjs +13 -3
- package/src/tui/session/session-api/lifecycle.mjs +17 -1
- package/src/tui/session/session-api/routes.mjs +61 -5
- package/src/tui/session/session-flow/drain.mjs +12 -3
- package/src/tui/session/session-flow/queue/entry-shape.mjs +2 -1
- package/src/tui/session/session-flow/queue/take-entries.mjs +3 -2
- package/src/tui/session/session-flow/steering.mjs +58 -24
- package/src/tui/session/tool-approval.mjs +26 -5
- package/src/tui/session/transcript-intake.mjs +8 -2
- package/src/tui/session/transcript-spill.mjs +5 -1
- package/src/tui/session/tui-steering-persist.mjs +65 -42
- package/src/tui/session/turn-live-call.mjs +64 -0
- package/src/tui/session/turn.mjs +34 -8
- package/src/session-runtime/automation-agents.mjs +0 -94
- package/src/session-runtime/automation-workflow.mjs +0 -59
|
@@ -19,7 +19,8 @@ import {
|
|
|
19
19
|
unlink as unlinkAsync,
|
|
20
20
|
writeFile as writeFileAsync,
|
|
21
21
|
} from 'node:fs/promises';
|
|
22
|
-
import {
|
|
22
|
+
import { tmpdir } from 'node:os';
|
|
23
|
+
import { dirname, basename, join, relative, isAbsolute, resolve } from 'node:path';
|
|
23
24
|
import { randomBytes } from 'node:crypto';
|
|
24
25
|
import { DEFAULT_BACKOFFS_MS, recordLockTiming, withFileLock, withFileLockSync } from './file-lock.mjs';
|
|
25
26
|
import {
|
|
@@ -164,7 +165,18 @@ async function fsyncPathAsync(path, tolerated) {
|
|
|
164
165
|
}
|
|
165
166
|
}
|
|
166
167
|
|
|
168
|
+
// Node sets NODE_TEST_CONTEXT in every `--test` process. A secret (credential)
|
|
169
|
+
// write from a test must land under the temp dir; anything else is a fixture
|
|
170
|
+
// about to overwrite a real profile, so fail loudly instead.
|
|
171
|
+
function assertTestSecretWriteSafe(filePath, opts) {
|
|
172
|
+
if (opts.secret !== true || !process.env.NODE_TEST_CONTEXT) return;
|
|
173
|
+
const rel = relative(resolve(tmpdir()).toLowerCase(), resolve(filePath).toLowerCase());
|
|
174
|
+
if (rel === '' || (!rel.startsWith('..') && !isAbsolute(rel))) return;
|
|
175
|
+
throw new Error(`refusing to write a secret outside the temp dir under node --test: ${filePath}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
167
178
|
export function writeFileAtomicSync(filePath, data, opts = {}) {
|
|
179
|
+
assertTestSecretWriteSafe(filePath, opts);
|
|
168
180
|
const run = () => {
|
|
169
181
|
const dir = dirname(filePath);
|
|
170
182
|
mkdirSync(dir, { recursive: true });
|
|
@@ -289,6 +301,7 @@ export function updateJsonAtomicSync(filePath, mutator, opts = {}) {
|
|
|
289
301
|
// block the host event loop on read/write/fsync/rename.
|
|
290
302
|
// Async writes retain atomic rename publication; truncate recovery is sync-only.
|
|
291
303
|
export async function writeFileAtomicAsync(filePath, data, opts = {}) {
|
|
304
|
+
assertTestSecretWriteSafe(filePath, opts);
|
|
292
305
|
const run = async () => {
|
|
293
306
|
const dir = dirname(filePath);
|
|
294
307
|
await mkdirAsync(dir, { recursive: true });
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
// The one-time 1.3.1 split of bots out of agents/: every definition the
|
|
2
|
+
// read-only rule (agent-kind.mjs) classifies as a bot is COPIED from
|
|
3
|
+
// agents/<id>/ to bots/<id>/ with its own model route, and Mixy's identity
|
|
4
|
+
// from agents/lead/ to bots/mixy/. Nothing under agents/ and no session file
|
|
5
|
+
// is ever moved, rewritten or deleted: the originals stay for rollback and
|
|
6
|
+
// the manifest (bots/migration.json) tells the agent catalog which agents/
|
|
7
|
+
// ids are bots now. Home threads keep their ids — a bot's is still
|
|
8
|
+
// sess_home_<id>, Mixy's still sess_home_lead (MIXY_LEGACY_HOME_SESSION_ID) —
|
|
9
|
+
// so every transcript stays whole and continuable.
|
|
10
|
+
//
|
|
11
|
+
// Every later start re-syncs (migrateBotsToOwnStore): it adds nothing and
|
|
12
|
+
// rewrites nothing unless agents/ gained or changed a bot since (a downgrade
|
|
13
|
+
// to 1.3.0 and back). The re-sync never overwrites a bots/ edit: a copy is
|
|
14
|
+
// replaced only when its fingerprint still equals the one recorded when it
|
|
15
|
+
// was made (bots/ untouched since) AND its agents/ original's fingerprint
|
|
16
|
+
// differs and has newer files; when both sides changed, both are kept (the
|
|
17
|
+
// bots/ copy in use, the 1.3.0 edit left in agents/, logged). Every copy or
|
|
18
|
+
// replacement is preceded by a user-data backup that holds agents/ and bots/.
|
|
19
|
+
//
|
|
20
|
+
// One process migrates at a time (an exclusive lock file beside the profile,
|
|
21
|
+
// reclaimed when its owner died); another starting meanwhile waits for it,
|
|
22
|
+
// or after LOCK_WAIT_MS reads the profile as it is this start. Publishing
|
|
23
|
+
// the manifest is the commit point: a failure before it undoes only this
|
|
24
|
+
// run's own copies and leaves the profile as it was (the next start tries
|
|
25
|
+
// again); nothing after it is undone. A replaced bots/<id> is set aside as
|
|
26
|
+
// `.<id>.<run>.old` until then; the manifest names the committed ones
|
|
27
|
+
// (`retired`), which any later start removes, while one it does not name is
|
|
28
|
+
// an interrupted run's and is put back.
|
|
29
|
+
import { createHash } from 'node:crypto';
|
|
30
|
+
import {
|
|
31
|
+
cpSync,
|
|
32
|
+
existsSync,
|
|
33
|
+
mkdirSync,
|
|
34
|
+
readdirSync,
|
|
35
|
+
readFileSync,
|
|
36
|
+
renameSync,
|
|
37
|
+
rmSync,
|
|
38
|
+
statSync,
|
|
39
|
+
writeFileSync,
|
|
40
|
+
} from 'node:fs';
|
|
41
|
+
import { basename, dirname, join, relative } from 'node:path';
|
|
42
|
+
import { withFileLockSync, writeJsonAtomicSync } from './atomic-file.mjs';
|
|
43
|
+
import { evidenceKind, homeThreadEvidence } from './agent-kind.mjs';
|
|
44
|
+
import { BOT_AGENT_KIND } from './agent-route-config.mjs';
|
|
45
|
+
import { readMarkdownDocument } from './markdown-frontmatter.mjs';
|
|
46
|
+
import {
|
|
47
|
+
BOT_MIGRATION_MANIFEST,
|
|
48
|
+
BOT_MIGRATION_VERSION,
|
|
49
|
+
BOTS_DIR,
|
|
50
|
+
MIXY_BOT_ID,
|
|
51
|
+
migratedBotIds,
|
|
52
|
+
readBotMigration,
|
|
53
|
+
} from './agent-store.mjs';
|
|
54
|
+
|
|
55
|
+
export { BOT_MIGRATION_MANIFEST, BOT_MIGRATION_VERSION, BOTS_DIR, MIXY_BOT_ID, migratedBotIds, readBotMigration };
|
|
56
|
+
/** Mixy's home thread keeps the id it always had: its messages never move. */
|
|
57
|
+
export const MIXY_LEGACY_HOME_SESSION_ID = 'sess_home_lead';
|
|
58
|
+
|
|
59
|
+
/** The cross-process migration lock, beside (never inside) bots/ and agents/. */
|
|
60
|
+
export const BOT_MIGRATION_LOCK = '.bot-split.lock';
|
|
61
|
+
const LOCK_WAIT_MS = 10_000;
|
|
62
|
+
/** What the split's backup carries besides the routine user data: every
|
|
63
|
+
* definition it copies from and every copy a re-sync can replace. */
|
|
64
|
+
export const BOT_SPLIT_BACKUP_DIRS = Object.freeze(['agents', BOTS_DIR]);
|
|
65
|
+
|
|
66
|
+
const DELETED_MARKER = '.deleted';
|
|
67
|
+
const ARCHIVED_FILE = 'AGENT.md.archived';
|
|
68
|
+
// Mixy's own files under agents/lead/ (its identity, persona, 일하는 방식, face).
|
|
69
|
+
const MIXY_IDENTITY_KEYS = ['name', 'description', 'avatar', 'connectors', 'created'];
|
|
70
|
+
|
|
71
|
+
function readJson(file) {
|
|
72
|
+
try {
|
|
73
|
+
const value = JSON.parse(readFileSync(file, 'utf8'));
|
|
74
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function writeJson(file, value) {
|
|
81
|
+
writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// The definition file a bot's kind is read from: AGENT.md, or the archived
|
|
85
|
+
// copy of a deleted bot (its chat is kept, so it moves with the others).
|
|
86
|
+
function definitionFile(dir) {
|
|
87
|
+
if (existsSync(join(dir, 'AGENT.md'))) return join(dir, 'AGENT.md');
|
|
88
|
+
if (existsSync(join(dir, DELETED_MARKER)) && existsSync(join(dir, ARCHIVED_FILE))) return join(dir, ARCHIVED_FILE);
|
|
89
|
+
return '';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The agents/ ids the rule classifies as bots (Mixy's `lead` never). */
|
|
93
|
+
export function classifyBotIds({ dataDir, shippedRoot }) {
|
|
94
|
+
const root = join(dataDir, 'agents');
|
|
95
|
+
let entries = [];
|
|
96
|
+
try {
|
|
97
|
+
entries = readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory());
|
|
98
|
+
} catch {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
const bots = [];
|
|
102
|
+
for (const { name: id } of entries) {
|
|
103
|
+
if (id === 'lead') continue;
|
|
104
|
+
const dir = join(root, id);
|
|
105
|
+
const file = definitionFile(dir);
|
|
106
|
+
if (!file) continue;
|
|
107
|
+
const doc = readMarkdownDocument(readFileSync(file, 'utf8'));
|
|
108
|
+
// The evidence rule itself, whatever the store says (a re-sync runs it
|
|
109
|
+
// with the store active).
|
|
110
|
+
const kind = evidenceKind({
|
|
111
|
+
frontmatterKind: doc.frontmatter.kind,
|
|
112
|
+
dataDir,
|
|
113
|
+
shippedRoot,
|
|
114
|
+
id,
|
|
115
|
+
manifest: readJson(join(dir, 'agent.json')) || {},
|
|
116
|
+
body: doc.body,
|
|
117
|
+
});
|
|
118
|
+
if (kind === BOT_AGENT_KIND) bots.push(id);
|
|
119
|
+
}
|
|
120
|
+
return bots.sort();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Whether agents/lead holds a Mixy the user made or used: a name, face,
|
|
124
|
+
* connectors, persona or 일하는 방식, or any conversation in its thread —
|
|
125
|
+
* never v1.3.0's untouched automatic `{"created": true}`. */
|
|
126
|
+
export function mixyToMigrate(dataDir) {
|
|
127
|
+
const dir = join(dataDir, 'agents', 'lead');
|
|
128
|
+
const manifest = readJson(join(dir, 'agent.json')) || {};
|
|
129
|
+
if (manifest.created !== true) return false;
|
|
130
|
+
const identity =
|
|
131
|
+
Boolean(String(manifest.name ?? '').trim()) ||
|
|
132
|
+
manifest.avatar != null ||
|
|
133
|
+
manifest.connectors != null ||
|
|
134
|
+
existsSync(join(dir, 'PERSONA.md')) ||
|
|
135
|
+
existsSync(join(dir, 'AGENT.md'));
|
|
136
|
+
return identity || homeThreadEvidence(dataDir, 'lead').chatted;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// The provider/model Mixy's thread runs on, or null.
|
|
140
|
+
function threadRoute(dataDir, sessionId) {
|
|
141
|
+
const record = readJson(join(dataDir, 'sessions', `${sessionId}.json`));
|
|
142
|
+
const provider = String(record?.provider || '').trim();
|
|
143
|
+
const model = String(record?.model || '').trim();
|
|
144
|
+
return provider && model ? { provider, model } : null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const completeRoute = (route) =>
|
|
148
|
+
route && typeof route === 'object' && String(route.provider || '').trim() && String(route.model || '').trim()
|
|
149
|
+
? route
|
|
150
|
+
: null;
|
|
151
|
+
|
|
152
|
+
// --- Content fingerprints (for the re-sync rule) ---
|
|
153
|
+
// A folder's files as one hash; agent.json without the keys only the store
|
|
154
|
+
// writes (a bot's route, Mixy's thread id and created flag), so changing a
|
|
155
|
+
// bot's model on 1.3.1 never counts as editing its definition.
|
|
156
|
+
const STORE_ONLY_KEYS = ['route', 'homeSessionId', 'created'];
|
|
157
|
+
|
|
158
|
+
function folderFiles(dir) {
|
|
159
|
+
try {
|
|
160
|
+
return readdirSync(dir, { recursive: true, withFileTypes: true })
|
|
161
|
+
.filter((entry) => entry.isFile())
|
|
162
|
+
.map((entry) => join(entry.parentPath ?? entry.path, entry.name))
|
|
163
|
+
.filter((file) => !file.endsWith('.lock'))
|
|
164
|
+
.sort();
|
|
165
|
+
} catch {
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// A JSON value with every object's keys sorted, nested ones included (a
|
|
171
|
+
// replacer array would drop every nested key not in the top-level list).
|
|
172
|
+
function canonical(value) {
|
|
173
|
+
if (Array.isArray(value)) return value.map(canonical);
|
|
174
|
+
if (!value || typeof value !== 'object') return value;
|
|
175
|
+
return Object.fromEntries(
|
|
176
|
+
Object.keys(value)
|
|
177
|
+
.sort()
|
|
178
|
+
.map((key) => [key, canonical(value[key])])
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function folderHash(dir) {
|
|
183
|
+
const hash = createHash('sha256');
|
|
184
|
+
for (const file of folderFiles(dir)) {
|
|
185
|
+
let bytes = readFileSync(file);
|
|
186
|
+
const manifest = basename(file) === 'agent.json' && dirname(file) === dir ? readJson(file) : null;
|
|
187
|
+
if (manifest) {
|
|
188
|
+
for (const key of STORE_ONLY_KEYS) delete manifest[key];
|
|
189
|
+
bytes = Buffer.from(JSON.stringify(canonical(manifest)));
|
|
190
|
+
}
|
|
191
|
+
hash
|
|
192
|
+
.update(`${relative(dir, file)}\0`)
|
|
193
|
+
.update(bytes)
|
|
194
|
+
.update('\0');
|
|
195
|
+
}
|
|
196
|
+
return hash.digest('hex').slice(0, 24);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function newestChange(dir) {
|
|
200
|
+
let newest = 0;
|
|
201
|
+
for (const file of folderFiles(dir)) newest = Math.max(newest, statSync(file).mtimeMs);
|
|
202
|
+
return newest;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// A staging or set-aside folder an earlier run left (`.<id>.<pid>-<time>.<kind>`).
|
|
206
|
+
// Swept under the migration lock, so no run that made one is still going.
|
|
207
|
+
const LEFTOVER = /^\.(.+)\.(\d+)-\d+\.(staging|old)$/;
|
|
208
|
+
const removeBestEffort = (path) => {
|
|
209
|
+
try {
|
|
210
|
+
rmSync(path, { recursive: true, force: true });
|
|
211
|
+
} catch {
|
|
212
|
+
// a later start removes it
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
// A staging folder goes; a set-aside `.old` the manifest names as retired is
|
|
216
|
+
// a committed swap's rest and goes; any other `.old` is the bot as it was
|
|
217
|
+
// before a run that never committed: it is put back (a failure here fails
|
|
218
|
+
// this run, which then leaves the profile as it is).
|
|
219
|
+
function sweepLeftovers(botsRoot, retired) {
|
|
220
|
+
for (const name of existsSync(botsRoot) ? readdirSync(botsRoot) : []) {
|
|
221
|
+
const match = LEFTOVER.exec(name);
|
|
222
|
+
if (!match) continue;
|
|
223
|
+
const path = join(botsRoot, name);
|
|
224
|
+
if (match[3] === 'staging' || retired.has(name)) {
|
|
225
|
+
removeBestEffort(path);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const target = join(botsRoot, match[1]);
|
|
229
|
+
if (existsSync(target)) {
|
|
230
|
+
const aside = join(botsRoot, `.${match[1]}.${match[2]}-0.staging`);
|
|
231
|
+
renameSync(target, aside);
|
|
232
|
+
removeBestEffort(aside);
|
|
233
|
+
}
|
|
234
|
+
renameSync(path, target);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// The finished split's record as on disk now (never a cached read: another
|
|
239
|
+
// process may have just written it).
|
|
240
|
+
function readManifestNow(botsRoot) {
|
|
241
|
+
const manifest = readJson(join(botsRoot, BOT_MIGRATION_MANIFEST));
|
|
242
|
+
return manifest?.version === BOT_MIGRATION_VERSION ? manifest : null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Runs the split, and on every later start the re-sync (`backup()` first
|
|
247
|
+
* whenever something is to be copied). `routeOf(id)` is the bot's route in
|
|
248
|
+
* the config (null when none); `leadRoute` the Lead's route at this time
|
|
249
|
+
* (Mixy's route when its thread names none).
|
|
250
|
+
*
|
|
251
|
+
* First run: every bot in agents/ is copied to bots/<id> (a bots/<id>
|
|
252
|
+
* already there — a concurrent run's, a hand-made or restored folder — is
|
|
253
|
+
* kept as it is and adopted), and a used Mixy to bots/mixy.
|
|
254
|
+
* Later runs (the manifest exists), so a downgrade to 1.3.0 and back loses
|
|
255
|
+
* nothing:
|
|
256
|
+
* - a bot 1.3.0 made in agents/ after the split is copied over as above;
|
|
257
|
+
* - a migrated bot whose agents/ original was edited after its copy (newer
|
|
258
|
+
* files, other content) while its bots/ copy was left as copied is
|
|
259
|
+
* copied again, keeping its bots/ route; when both sides changed the
|
|
260
|
+
* bots/ copy wins and the edit stays in agents/ (logged);
|
|
261
|
+
* - a migrated bot whose bots/<id> is gone (deleted for good) never comes
|
|
262
|
+
* back from its agents/ original;
|
|
263
|
+
* - Mixy, once made or used in agents/lead, fills what bots/mixy lacks.
|
|
264
|
+
* Every copy is staged and swapped in by rename; a failure before the
|
|
265
|
+
* manifest is published puts back what this run replaced, removes what it
|
|
266
|
+
* added, writes no manifest and leaves agents/ and every session untouched.
|
|
267
|
+
*
|
|
268
|
+
* Runs under the cross-process migration lock: a second process waits up to
|
|
269
|
+
* `lockWaitMs`, then reads the profile as it is (`{ failed, busy }`).
|
|
270
|
+
* `afterPublish` is an interleaving point for tests (a failure there is one
|
|
271
|
+
* after the commit point).
|
|
272
|
+
*/
|
|
273
|
+
export function migrateBotsToOwnStore(options) {
|
|
274
|
+
const { dataDir, log, lockWaitMs = LOCK_WAIT_MS } = options;
|
|
275
|
+
try {
|
|
276
|
+
return withFileLockSync(join(dataDir, BOT_MIGRATION_LOCK), () => migrateLocked(options), {
|
|
277
|
+
timeoutMs: lockWaitMs,
|
|
278
|
+
});
|
|
279
|
+
} catch (error) {
|
|
280
|
+
// Only the lock fails here (migrateLocked reports its own failures).
|
|
281
|
+
const done = readBotMigration(dataDir);
|
|
282
|
+
log?.(`[bot-split] another start is migrating; this one reads the profile as it is: ${error?.message || error}`);
|
|
283
|
+
return { skipped: false, failed: true, busy: true, bots: done?.bots || [], mixy: done?.mixy === true };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function migrateLocked({ dataDir, shippedRoot, routeOf = () => null, leadRoute = null, backup, log, afterPublish }) {
|
|
288
|
+
const botsRoot = join(dataDir, BOTS_DIR);
|
|
289
|
+
const done = readManifestNow(botsRoot);
|
|
290
|
+
const runTag = `${process.pid}-${Date.now()}`;
|
|
291
|
+
// What this run changed, undone in reverse on a failure before the commit.
|
|
292
|
+
const changes = [];
|
|
293
|
+
let backedUp = false;
|
|
294
|
+
const beforeCopy = () => {
|
|
295
|
+
if (backedUp) return;
|
|
296
|
+
backedUp = true;
|
|
297
|
+
backup?.();
|
|
298
|
+
};
|
|
299
|
+
// Swaps a staged folder in for bots/<id> (atomic renames); the replaced
|
|
300
|
+
// folder is set aside as `.old` until the commit.
|
|
301
|
+
const swapIn = (id, staging, { replace }) => {
|
|
302
|
+
const target = join(botsRoot, id);
|
|
303
|
+
let old = '';
|
|
304
|
+
if (replace && existsSync(target)) {
|
|
305
|
+
old = join(botsRoot, `.${id}.${runTag}.old`);
|
|
306
|
+
renameSync(target, old);
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
renameSync(staging, target);
|
|
310
|
+
} catch (error) {
|
|
311
|
+
removeBestEffort(staging);
|
|
312
|
+
// Should this fail too, the next start's sweep puts the `.old` back.
|
|
313
|
+
if (old) renameSync(old, target);
|
|
314
|
+
throw error;
|
|
315
|
+
}
|
|
316
|
+
changes.push({ target, old });
|
|
317
|
+
};
|
|
318
|
+
const stage = (id, source) => {
|
|
319
|
+
const staging = join(botsRoot, `.${id}.${runTag}.staging`);
|
|
320
|
+
rmSync(staging, { recursive: true, force: true });
|
|
321
|
+
if (source) cpSync(source, staging, { recursive: true });
|
|
322
|
+
else mkdirSync(staging, { recursive: true });
|
|
323
|
+
return staging;
|
|
324
|
+
};
|
|
325
|
+
let next;
|
|
326
|
+
try {
|
|
327
|
+
mkdirSync(botsRoot, { recursive: true });
|
|
328
|
+
sweepLeftovers(botsRoot, new Set(done?.retired || []));
|
|
329
|
+
// Committed set-asides the sweep could not remove stay named for a later start.
|
|
330
|
+
const retired = (done?.retired || []).filter((name) => existsSync(join(botsRoot, name)));
|
|
331
|
+
const copies = { ...(done?.copies || {}) };
|
|
332
|
+
const bots = new Set(done?.bots || []);
|
|
333
|
+
const record = (id, sourceDir) => {
|
|
334
|
+
copies[id] = { source: folderHash(sourceDir), copy: folderHash(join(botsRoot, id)), at: Date.now() };
|
|
335
|
+
};
|
|
336
|
+
// 1. Bots in agents/ not yet in the store.
|
|
337
|
+
for (const id of classifyBotIds({ dataDir, shippedRoot })) {
|
|
338
|
+
if (bots.has(id)) continue;
|
|
339
|
+
const source = join(dataDir, 'agents', id);
|
|
340
|
+
if (!existsSync(join(botsRoot, id))) {
|
|
341
|
+
beforeCopy();
|
|
342
|
+
const route = completeRoute(routeOf(id));
|
|
343
|
+
const staging = stage(id, source);
|
|
344
|
+
const manifest = readJson(join(staging, 'agent.json')) || {};
|
|
345
|
+
writeJson(join(staging, 'agent.json'), { ...manifest, ...(route ? { route } : {}) });
|
|
346
|
+
swapIn(id, staging, { replace: false });
|
|
347
|
+
}
|
|
348
|
+
bots.add(id);
|
|
349
|
+
record(id, source);
|
|
350
|
+
}
|
|
351
|
+
// 2. Migrated bots edited in agents/ after their copy (a 1.3.0 round trip).
|
|
352
|
+
for (const id of done ? [...bots] : []) {
|
|
353
|
+
const source = join(dataDir, 'agents', id);
|
|
354
|
+
const target = join(botsRoot, id);
|
|
355
|
+
const entry = copies[id];
|
|
356
|
+
if (!entry || !existsSync(target) || !existsSync(source)) continue;
|
|
357
|
+
const sourceChanged = folderHash(source) !== entry.source && newestChange(source) > entry.at;
|
|
358
|
+
if (!sourceChanged) continue;
|
|
359
|
+
if (folderHash(target) !== entry.copy) {
|
|
360
|
+
log?.(`[bot-split] ${id}: edited in both agents/ and bots/; the bots/ copy is kept`);
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
beforeCopy();
|
|
364
|
+
const route = readJson(join(target, 'agent.json'))?.route;
|
|
365
|
+
const staging = stage(id, source);
|
|
366
|
+
const manifest = readJson(join(staging, 'agent.json')) || {};
|
|
367
|
+
writeJson(join(staging, 'agent.json'), { ...manifest, ...(completeRoute(route) ? { route } : {}) });
|
|
368
|
+
swapIn(id, staging, { replace: true });
|
|
369
|
+
record(id, source);
|
|
370
|
+
}
|
|
371
|
+
// 3. Mixy: made or used in agents/lead; bots/mixy keeps what it has.
|
|
372
|
+
let mixy = done?.mixy === true;
|
|
373
|
+
const leadDir = join(dataDir, 'agents', 'lead');
|
|
374
|
+
const mixyDir = join(botsRoot, MIXY_BOT_ID);
|
|
375
|
+
const mixyEntry = copies[MIXY_BOT_ID];
|
|
376
|
+
const leadEdited =
|
|
377
|
+
mixyEntry &&
|
|
378
|
+
existsSync(mixyDir) &&
|
|
379
|
+
folderHash(leadDir) !== mixyEntry.source &&
|
|
380
|
+
newestChange(leadDir) > mixyEntry.at;
|
|
381
|
+
if (mixyToMigrate(dataDir) && (!mixyEntry || leadEdited)) {
|
|
382
|
+
const current = existsSync(mixyDir) ? readJson(join(mixyDir, 'agent.json')) || {} : {};
|
|
383
|
+
const untouched = mixyEntry && folderHash(mixyDir) === mixyEntry.copy;
|
|
384
|
+
if (mixyEntry && !untouched) {
|
|
385
|
+
log?.('[bot-split] Mixy: edited in both agents/lead and bots/mixy; bots/mixy is kept');
|
|
386
|
+
} else {
|
|
387
|
+
beforeCopy();
|
|
388
|
+
// A first copy fills what bots/mixy lacks (the host may have written
|
|
389
|
+
// its flag or route there); a re-sync takes agents/lead's identity.
|
|
390
|
+
const staging = stage(MIXY_BOT_ID, existsSync(mixyDir) && !mixyEntry ? mixyDir : null);
|
|
391
|
+
for (const name of folderFiles(leadDir).map((file) => relative(leadDir, file))) {
|
|
392
|
+
if (name === 'agent.json' || (existsSync(join(staging, name)) && !mixyEntry)) continue;
|
|
393
|
+
mkdirSync(dirname(join(staging, name)), { recursive: true });
|
|
394
|
+
cpSync(join(leadDir, name), join(staging, name));
|
|
395
|
+
}
|
|
396
|
+
const lead = readJson(join(leadDir, 'agent.json')) || {};
|
|
397
|
+
const identity = Object.fromEntries(
|
|
398
|
+
MIXY_IDENTITY_KEYS.filter((key) => key in lead).map((key) => [key, lead[key]])
|
|
399
|
+
);
|
|
400
|
+
const route =
|
|
401
|
+
completeRoute(current.route) || threadRoute(dataDir, MIXY_LEGACY_HOME_SESSION_ID) || completeRoute(leadRoute);
|
|
402
|
+
writeJson(join(staging, 'agent.json'), {
|
|
403
|
+
...identity,
|
|
404
|
+
...(mixyEntry ? {} : current),
|
|
405
|
+
...(typeof current.created === 'boolean' ? { created: current.created } : {}),
|
|
406
|
+
...(route ? { route } : {}),
|
|
407
|
+
homeSessionId: MIXY_LEGACY_HOME_SESSION_ID,
|
|
408
|
+
});
|
|
409
|
+
swapIn(MIXY_BOT_ID, staging, { replace: true });
|
|
410
|
+
record(MIXY_BOT_ID, leadDir);
|
|
411
|
+
mixy = true;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
retired.push(...changes.filter(({ old }) => old).map(({ old }) => basename(old)));
|
|
415
|
+
next = {
|
|
416
|
+
version: BOT_MIGRATION_VERSION,
|
|
417
|
+
completedAt: done?.completedAt || new Date().toISOString(),
|
|
418
|
+
bots: [...bots].sort(),
|
|
419
|
+
mixy,
|
|
420
|
+
mixyHomeSessionId: mixy ? MIXY_LEGACY_HOME_SESSION_ID : null,
|
|
421
|
+
copies,
|
|
422
|
+
...(retired.length ? { retired: retired.sort() } : {}),
|
|
423
|
+
};
|
|
424
|
+
// The commit point: once this rename lands, nothing of this run is undone.
|
|
425
|
+
if (!done || changes.length || JSON.stringify(next) !== JSON.stringify(done)) {
|
|
426
|
+
writeJsonAtomicSync(join(botsRoot, BOT_MIGRATION_MANIFEST), next, { mode: 0o644 });
|
|
427
|
+
}
|
|
428
|
+
} catch (error) {
|
|
429
|
+
// Undo this run: what it added goes, what it replaced comes back. A step
|
|
430
|
+
// that fails leaves an unretired `.old` the next start's sweep puts back.
|
|
431
|
+
for (const { target, old } of changes.reverse()) {
|
|
432
|
+
try {
|
|
433
|
+
const aside = join(botsRoot, `.${basename(target)}.${runTag}.staging`);
|
|
434
|
+
renameSync(target, aside);
|
|
435
|
+
if (old) renameSync(old, target);
|
|
436
|
+
removeBestEffort(aside);
|
|
437
|
+
} catch {
|
|
438
|
+
// the leftover sweep of a later start finishes it
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
for (const name of existsSync(botsRoot) ? readdirSync(botsRoot) : []) {
|
|
442
|
+
if (name.endsWith(`.${runTag}.staging`)) removeBestEffort(join(botsRoot, name));
|
|
443
|
+
}
|
|
444
|
+
log?.(`[bot-split] migration failed; the profile is unchanged: ${error?.message || error}`);
|
|
445
|
+
return { skipped: false, failed: true, bots: done?.bots || [], mixy: done?.mixy === true };
|
|
446
|
+
}
|
|
447
|
+
// Committed. Removing the set-aside copies is best-effort: the manifest
|
|
448
|
+
// names them as retired, so a later start removes what is left.
|
|
449
|
+
try {
|
|
450
|
+
afterPublish?.();
|
|
451
|
+
for (const { old } of changes) if (old) rmSync(old, { recursive: true, force: true });
|
|
452
|
+
} catch (error) {
|
|
453
|
+
log?.(`[bot-split] committed; leftover cleanup is retried next start: ${error?.message || error}`);
|
|
454
|
+
}
|
|
455
|
+
if (done && !changes.length) return { skipped: true, bots: next.bots, mixy: next.mixy };
|
|
456
|
+
return { skipped: false, bots: next.bots, mixy: next.mixy, changed: changes.map(({ target }) => basename(target)) };
|
|
457
|
+
}
|
|
@@ -43,6 +43,25 @@ function applyField(current, path, change) {
|
|
|
43
43
|
return next;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
const isSet = (value) => value !== undefined && value !== null && value !== '';
|
|
47
|
+
|
|
48
|
+
function valueAt(source, path) {
|
|
49
|
+
let value = source;
|
|
50
|
+
for (const key of path) {
|
|
51
|
+
if (!isPlainObject(value) || !Object.hasOwn(value, key)) return undefined;
|
|
52
|
+
value = value[key];
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A change with `unlessSet: [path…]` is skipped when the stored value at that
|
|
58
|
+
* path is already set. It is judged against `current`, the state before the
|
|
59
|
+
* whole patch, so the changes of one patch that share a guard stand or fall
|
|
60
|
+
* together whatever their order. */
|
|
46
61
|
export function applyConfigPatch(current, changes) {
|
|
47
|
-
return changes.reduce(
|
|
62
|
+
return changes.reduce(
|
|
63
|
+
(next, change) =>
|
|
64
|
+
change.unlessSet && isSet(valueAt(current, change.unlessSet)) ? next : applyField(next, change.path, change),
|
|
65
|
+
current
|
|
66
|
+
);
|
|
48
67
|
}
|
|
@@ -328,7 +328,14 @@ function readAll() {
|
|
|
328
328
|
// marker yet) is a recovery case too; without a verifiable backup reads stay
|
|
329
329
|
// empty and RMW writes refuse (see readAllForRmW).
|
|
330
330
|
if (isUserDataRecoveryExpected(resolvePluginData())) {
|
|
331
|
-
|
|
331
|
+
let restored = null;
|
|
332
|
+
try {
|
|
333
|
+
restored = loadLatestMixdogConfigFromBackup(resolvePluginData());
|
|
334
|
+
} catch (err) {
|
|
335
|
+
// A transient backup read failure: this read stays empty and the next
|
|
336
|
+
// one retries (an RMW write refuses instead).
|
|
337
|
+
process.stderr.write(`[config] backup restore lookup failed: ${err.message}\n`);
|
|
338
|
+
}
|
|
332
339
|
if (restored && isPlainObject(restored)) {
|
|
333
340
|
const config = canonicalizeUnifiedConfig(restored);
|
|
334
341
|
persistRestoredConfig(config);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// What a multi-account connector server tells the model about itself (MCP
|
|
2
|
+
// `instructions`): the account it acts for, and to ask which one when the
|
|
3
|
+
// user did not say and several are connected. The deferred-tool manifest
|
|
4
|
+
// (context/deferred-tools.mjs) reads the leading "Account: <address>" back.
|
|
5
|
+
|
|
6
|
+
/** The MCP instructions of a `service` server acting for `account` ('' when unknown). */
|
|
7
|
+
export function accountInstructions(service, account) {
|
|
8
|
+
if (!account) return undefined;
|
|
9
|
+
return `Account: ${account}. This server acts only for this ${service} account; other ${service} accounts are separate servers. If several are connected and the user did not name one, ask which account before acting.`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** The account a server's instructions name, or ''. */
|
|
13
|
+
export function accountOfInstructions(instructions) {
|
|
14
|
+
return /^Account: (\S+?)\. /.exec(String(instructions || ''))?.[1] || '';
|
|
15
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export type ConnectorBackend =
|
|
2
|
+
| {
|
|
3
|
+
kind: 'oauth';
|
|
4
|
+
url: string;
|
|
5
|
+
/** A pre-registered client the runtime supplies instead of registration. */
|
|
6
|
+
oauthClient?: 'google';
|
|
7
|
+
scopes?: readonly string[];
|
|
8
|
+
askTools?: readonly string[];
|
|
9
|
+
}
|
|
10
|
+
| {
|
|
11
|
+
kind: 'apiKey';
|
|
12
|
+
url?: string;
|
|
13
|
+
header?: string;
|
|
14
|
+
prefix?: string;
|
|
15
|
+
command?: string;
|
|
16
|
+
args?: readonly string[];
|
|
17
|
+
env?: string;
|
|
18
|
+
keyUrl: string;
|
|
19
|
+
placeholder?: string;
|
|
20
|
+
/** Tools that ask before they run unless the agent set them. */
|
|
21
|
+
askTools?: readonly string[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export interface ConnectorCatalogEntry {
|
|
25
|
+
id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
backend: ConnectorBackend;
|
|
28
|
+
/** Offered only after the user enters their own Google OAuth client. */
|
|
29
|
+
ownKey?: boolean;
|
|
30
|
+
/** Curated deferred-tool manifest line (connector-triggers.mjs). */
|
|
31
|
+
trigger: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const CONNECTOR_CATALOG: readonly ConnectorCatalogEntry[];
|
|
35
|
+
export const GOOGLE_SCOPES: Readonly<Record<'gmail' | 'calendar' | 'drive', readonly string[]>>;
|
|
36
|
+
export const CONNECTOR_IDS: readonly string[];
|
|
37
|
+
export function connectorById(id: unknown): ConnectorCatalogEntry | null;
|
|
38
|
+
export function connectorOfServer(cfg: unknown): string;
|
|
39
|
+
export function connectorToolDefault(connectorId: string, tool: string): 'auto' | 'needs_approval';
|
|
40
|
+
export function connectorServerInput(
|
|
41
|
+
entry: ConnectorCatalogEntry,
|
|
42
|
+
name: string,
|
|
43
|
+
key?: string,
|
|
44
|
+
options?: { extraAccount?: boolean }
|
|
45
|
+
): Record<string, unknown>;
|
|
46
|
+
export function connectorMultiAccount(entry: ConnectorCatalogEntry | null | undefined): boolean;
|
|
47
|
+
export const GMAIL_APP_PASSWORD: Readonly<{ url: string; accountHeader: string; helpUrl: string }>;
|
|
48
|
+
export function gmailAddress(value: unknown): string;
|
|
49
|
+
export function gmailAppPasswordValue(value: unknown): string;
|
|
50
|
+
export function gmailAppPasswordAccount(cfg: unknown): string;
|
|
51
|
+
export function gmailAppPasswordServerInput(name: string, email: string, password: string): Record<string, unknown>;
|