mixdog 1.2.6 → 1.3.0
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 +19 -0
- 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 +7 -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 +11 -4
- package/src/defaults/skills/setup-guide/SKILL.md +175 -0
- package/src/defaults/skills/setup-guide/model-recommendations.json +333 -0
- package/src/lib/rules-builder.cjs +55 -6
- 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 +21 -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 +182 -13
- package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +4 -2
- package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +215 -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-login.mjs +10 -3
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
- 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/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 +12 -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 +10 -4
- package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +4 -1
- package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +50 -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 +162 -0
- package/src/runtime/agent/orchestrator/session/agent-room.mjs +641 -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 +165 -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/pre-dispatch-deny.mjs +10 -0
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +16 -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 +7 -0
- 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/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 +27 -5
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +55 -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/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 +24 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +32 -5
- package/src/runtime/agent/orchestrator/session/routine-result.d.mts +70 -0
- package/src/runtime/agent/orchestrator/session/routine-result.mjs +277 -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 +44 -1
- 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/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/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-route-config.d.mts +3 -0
- package/src/runtime/shared/agent-route-config.mjs +16 -0
- package/src/runtime/shared/atomic-file.mjs +14 -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 +52 -0
- package/src/runtime/shared/connector-catalog.mjs +230 -0
- package/src/runtime/shared/connector-triggers.d.mts +3 -0
- package/src/runtime/shared/connector-triggers.mjs +43 -0
- package/src/runtime/shared/google-oauth-client.mjs +24 -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/plain-preview.d.mts +1 -0
- package/src/runtime/shared/plain-preview.mjs +28 -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/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 +56 -0
- package/src/runtime/shared/user-data-guard.mjs +114 -36
- package/src/session-runtime/agent-home-delivery.mjs +459 -0
- package/src/session-runtime/agent-profile-changes.mjs +21 -0
- package/src/session-runtime/agent-room-delivery.mjs +92 -0
- package/src/session-runtime/agent-task-sessions.mjs +200 -0
- package/src/session-runtime/boot/apis.mjs +50 -18
- package/src/session-runtime/boot/facade.mjs +5 -8
- package/src/session-runtime/boot/infrastructure.mjs +28 -17
- package/src/session-runtime/boot/tools.mjs +3 -0
- package/src/session-runtime/channel-config-api.mjs +53 -5
- 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/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 +31 -2
- 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/set-route.mjs +14 -3
- 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/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 +10 -0
- 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/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 +68 -6
- package/src/session-runtime/setup-tool/extended-actions.mjs +72 -14
- package/src/session-runtime/setup-tool/settings-contract.mjs +8 -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 +238 -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 +3 -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 +556 -22
- 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 +11 -2
- package/src/standalone/daemon.mjs +18 -4
- package/src/standalone/session-protocol.mjs +27 -0
- package/src/standalone/session-service/session-calls/session-turns.mjs +9 -0
- package/src/standalone/session-service.mjs +19 -0
- package/src/tui/app/slash-commands.mjs +20 -0
- package/src/tui/dist/index.mjs +164 -86
- package/src/tui/session/context-state.mjs +6 -1
- package/src/tui/session/notice-surface.mjs +35 -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 +48 -3
- 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 +6 -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 +56 -0
- package/src/tui/session/turn.mjs +22 -6
- package/src/session-runtime/automation-agents.mjs +0 -94
- package/src/session-runtime/automation-workflow.mjs +0 -59
|
@@ -8,6 +8,15 @@ import { openInBrowser } from '../runtime/shared/open-url.mjs';
|
|
|
8
8
|
// Settled flows stay readable long enough for a poller to observe the result.
|
|
9
9
|
const SETTLED_FLOW_TTL_MS = 10 * 60_000;
|
|
10
10
|
const NOT_COMPLETED = 'MCP OAuth sign-in was not completed';
|
|
11
|
+
export const MCP_OAUTH_LOGIN_TIMED_OUT = 'MCP OAuth sign-in timed out';
|
|
12
|
+
// A sign-in nobody finishes ends here, ahead of the OAuth layer's own 5-minute
|
|
13
|
+
// abort, so its failure is reported as the timeout it is.
|
|
14
|
+
const LOGIN_DEADLINE_MS = 5 * 60_000 - 10_000;
|
|
15
|
+
// The longest a status read waits for a change (local callers only).
|
|
16
|
+
const MAX_STATUS_WAIT_MS = 15_000;
|
|
17
|
+
// How a sign-in treats the account (mcp/oauth-account.mjs): a normal
|
|
18
|
+
// authorization, a repair of the same connection, or a transactional switch.
|
|
19
|
+
const MODES = new Set(['connect', 'reauth', 'switch']);
|
|
11
20
|
|
|
12
21
|
const NOT_PENDING = 'MCP OAuth sign-in is no longer pending';
|
|
13
22
|
|
|
@@ -19,17 +28,36 @@ const NOT_PENDING = 'MCP OAuth sign-in is no longer pending';
|
|
|
19
28
|
const flows = new Map();
|
|
20
29
|
|
|
21
30
|
// A pending flow keeps its authorization URL readable: a client polling the
|
|
22
|
-
// status (remote sign-in) still shows the page to open.
|
|
31
|
+
// status (remote sign-in) still shows the page to open. `step` names what a
|
|
32
|
+
// pending flow waits for: `browser` (the sign-in page), `connecting` (the
|
|
33
|
+
// callback arrived; tokens are exchanged), `tools` (signed in; the server
|
|
34
|
+
// connects and lists its tools).
|
|
23
35
|
const view = (flow) => ({
|
|
24
36
|
flowId: flow.flowId,
|
|
25
37
|
state: flow.state,
|
|
38
|
+
...(flow.state === 'pending' ? { step: flow.step } : {}),
|
|
26
39
|
...(flow.state === 'pending' && flow.url ? { url: flow.url } : {}),
|
|
27
40
|
...(flow.error ? { error: flow.error } : {}),
|
|
41
|
+
...(flow.reason ? { reason: flow.reason } : {}),
|
|
28
42
|
});
|
|
43
|
+
const versionOf = (flow) => `${flow.state}:${flow.step}`;
|
|
44
|
+
// Wake every status read waiting on this flow.
|
|
45
|
+
function changed(flow) {
|
|
46
|
+
const waiters = flow.waiters;
|
|
47
|
+
flow.waiters = [];
|
|
48
|
+
for (const wake of waiters) wake();
|
|
49
|
+
}
|
|
50
|
+
function setStep(flow, step) {
|
|
51
|
+
if (flow.state !== 'pending' || flow.step === step) return;
|
|
52
|
+
flow.step = step;
|
|
53
|
+
changed(flow);
|
|
54
|
+
}
|
|
29
55
|
function settle(flow, state, error = null) {
|
|
30
56
|
if (flow.state !== 'pending') return;
|
|
31
57
|
flow.state = state;
|
|
32
58
|
if (state === 'failed') flow.error = error || NOT_COMPLETED;
|
|
59
|
+
clearTimeout(flow.deadline);
|
|
60
|
+
changed(flow);
|
|
33
61
|
setTimeout(() => flows.delete(flow.flowId), SETTLED_FLOW_TTL_MS).unref?.();
|
|
34
62
|
}
|
|
35
63
|
function flowOf(flowId) {
|
|
@@ -40,19 +68,23 @@ function flowOf(flowId) {
|
|
|
40
68
|
async function cancelFlow(flow) {
|
|
41
69
|
if (flow.state === 'pending') {
|
|
42
70
|
flow.cancelled = true;
|
|
43
|
-
flow.
|
|
71
|
+
flow.abort();
|
|
44
72
|
await flow.done;
|
|
45
73
|
}
|
|
46
74
|
return view(flow);
|
|
47
75
|
}
|
|
48
76
|
|
|
49
77
|
/**
|
|
50
|
-
* `connect(name,
|
|
51
|
-
* `
|
|
78
|
+
* `connect(name, launch)` reconnects the server interactively, calling
|
|
79
|
+
* `launch(url)` when a login starts, and resolves `{ connected, error }`.
|
|
52
80
|
* `cancel(name)` aborts that server's pending login. `completeCallback(name,
|
|
53
81
|
* callbackUrl)` answers it with a pasted redirect URL (throws a fixed message
|
|
54
|
-
* for an invalid one). `
|
|
55
|
-
*
|
|
82
|
+
* for an invalid one). `switchAccount(name, launch, attempt)` runs an account
|
|
83
|
+
* switch instead (mode `switch`): it registers `attempt.abort` / `attempt.answer`
|
|
84
|
+
* and resolves `{ connected, error }` only after committing (or rolling back)
|
|
85
|
+
* the new sign-in. `launch` carries `accountMode` and the progress hooks
|
|
86
|
+
* `onCallback` / `onAuthorized` the OAuth layer calls. `onComplete(name)` runs
|
|
87
|
+
* after a successful sign-in, once the flow already reports complete.
|
|
56
88
|
*/
|
|
57
89
|
export function createMcpOAuthLogins({
|
|
58
90
|
connect,
|
|
@@ -60,8 +92,12 @@ export function createMcpOAuthLogins({
|
|
|
60
92
|
completeCallback = () => {
|
|
61
93
|
throw new Error(NOT_PENDING);
|
|
62
94
|
},
|
|
95
|
+
switchAccount = async () => {
|
|
96
|
+
throw new Error('Switching the account is not supported here');
|
|
97
|
+
},
|
|
63
98
|
onComplete = async () => {},
|
|
64
99
|
openBrowser = openInBrowser,
|
|
100
|
+
loginTimeoutMs = LOGIN_DEADLINE_MS,
|
|
65
101
|
}) {
|
|
66
102
|
// name -> tail of that server's serialized begin/cancel/sign-out steps, so
|
|
67
103
|
// replacing a pending flow is atomic and exactly one can be pending.
|
|
@@ -89,10 +125,11 @@ export function createMcpOAuthLogins({
|
|
|
89
125
|
Promise.all([...flows.values()].filter((flow) => flow.owner === owner && flow.name === name).map(cancelFlow));
|
|
90
126
|
|
|
91
127
|
async function begin(name, options = {}) {
|
|
92
|
-
const { openBrowser: hostBrowser } = options || {};
|
|
128
|
+
const { openBrowser: hostBrowser, mode = 'connect' } = options || {};
|
|
93
129
|
if (hostBrowser !== undefined && typeof hostBrowser !== 'boolean') {
|
|
94
130
|
throw new TypeError('openBrowser must be a boolean');
|
|
95
131
|
}
|
|
132
|
+
if (!MODES.has(mode)) throw new TypeError('mode must be connect, reauth or switch');
|
|
96
133
|
let announceUrl;
|
|
97
134
|
const urlReady = new Promise((resolve) => {
|
|
98
135
|
announceUrl = resolve;
|
|
@@ -105,10 +142,17 @@ export function createMcpOAuthLogins({
|
|
|
105
142
|
flowId: `mcp_oauth_${randomUUID()}`,
|
|
106
143
|
name,
|
|
107
144
|
owner,
|
|
145
|
+
mode,
|
|
108
146
|
state: 'pending',
|
|
147
|
+
step: 'starting',
|
|
109
148
|
error: null,
|
|
149
|
+
reason: null,
|
|
110
150
|
cancelled: false,
|
|
151
|
+
timedOut: false,
|
|
111
152
|
url: null,
|
|
153
|
+
waiters: [],
|
|
154
|
+
abort: () => owner.cancel(name),
|
|
155
|
+
answer: (callbackUrl) => owner.completeCallback(name, callbackUrl),
|
|
112
156
|
};
|
|
113
157
|
flows.set(flow.flowId, flow);
|
|
114
158
|
const launch = async (url) => {
|
|
@@ -117,22 +161,51 @@ export function createMcpOAuthLogins({
|
|
|
117
161
|
if (flow.cancelled) throw new Error('MCP OAuth login cancelled');
|
|
118
162
|
flow.awaitingCallback = true;
|
|
119
163
|
flow.url = url;
|
|
164
|
+
setStep(flow, 'browser');
|
|
120
165
|
announceUrl(url);
|
|
121
166
|
if (hostBrowser !== false) openBrowser(url);
|
|
167
|
+
// Nobody finished the sign-in page in time.
|
|
168
|
+
flow.deadline = setTimeout(() => {
|
|
169
|
+
if (flow.state !== 'pending' || flow.step !== 'browser') return;
|
|
170
|
+
flow.timedOut = flow.cancelled = true;
|
|
171
|
+
flow.abort();
|
|
172
|
+
}, loginTimeoutMs);
|
|
173
|
+
flow.deadline.unref?.();
|
|
122
174
|
};
|
|
175
|
+
launch.accountMode = mode;
|
|
176
|
+
launch.onCallback = () => setStep(flow, 'connecting');
|
|
177
|
+
launch.onAuthorized = () => setStep(flow, 'tools');
|
|
123
178
|
flow.done = (async () => {
|
|
124
179
|
try {
|
|
125
|
-
const outcome =
|
|
126
|
-
|
|
180
|
+
const outcome =
|
|
181
|
+
mode === 'switch'
|
|
182
|
+
? await switchAccount(name, launch, {
|
|
183
|
+
set abort(fn) {
|
|
184
|
+
flow.abort = fn;
|
|
185
|
+
},
|
|
186
|
+
set answer(fn) {
|
|
187
|
+
flow.answer = fn;
|
|
188
|
+
},
|
|
189
|
+
})
|
|
190
|
+
: await connect(name, launch);
|
|
191
|
+
if (flow.timedOut) {
|
|
192
|
+
flow.reason = 'timeout';
|
|
193
|
+
settle(flow, 'failed', MCP_OAUTH_LOGIN_TIMED_OUT);
|
|
194
|
+
} else if (flow.cancelled) settle(flow, 'cancelled');
|
|
127
195
|
else if (outcome?.connected) {
|
|
128
|
-
|
|
129
|
-
|
|
196
|
+
settle(flow, 'complete');
|
|
197
|
+
// The other runtimes reconnect after the flow already reports
|
|
198
|
+
// complete: the caller never waits for every peer's reconnect, and
|
|
199
|
+
// a failed follow-up never undoes the sign-in.
|
|
200
|
+
void Promise.resolve()
|
|
130
201
|
.then(() => onComplete(name))
|
|
131
202
|
.catch(() => {});
|
|
132
|
-
settle(flow, 'complete');
|
|
133
203
|
} else settle(flow, 'failed', outcome?.error);
|
|
134
204
|
} catch (error) {
|
|
135
|
-
|
|
205
|
+
if (flow.timedOut) {
|
|
206
|
+
flow.reason = 'timeout';
|
|
207
|
+
settle(flow, 'failed', MCP_OAUTH_LOGIN_TIMED_OUT);
|
|
208
|
+
} else settle(flow, flow.cancelled ? 'cancelled' : 'failed', error?.message || String(error));
|
|
136
209
|
}
|
|
137
210
|
})();
|
|
138
211
|
return flow;
|
|
@@ -147,7 +220,31 @@ export function createMcpOAuthLogins({
|
|
|
147
220
|
|
|
148
221
|
return {
|
|
149
222
|
begin,
|
|
150
|
-
|
|
223
|
+
/** The flow's view. With `{ since, waitMs }` (the `state:step` a caller
|
|
224
|
+
* last saw) the read resolves on the flow's next change, or after at most
|
|
225
|
+
* `waitMs` unchanged: the caller follows each step as it happens. */
|
|
226
|
+
status(flowId, options = {}) {
|
|
227
|
+
const flow = flowOf(flowId);
|
|
228
|
+
const { since, waitMs } = options || {};
|
|
229
|
+
if (since === undefined) return view(flow);
|
|
230
|
+
if (typeof since !== 'string') throw new TypeError('since must be a string');
|
|
231
|
+
if (!Number.isFinite(waitMs) || waitMs < 0) throw new TypeError('waitMs must be a non-negative number');
|
|
232
|
+
if (since !== versionOf(flow) || flow.state !== 'pending' || waitMs === 0) return view(flow);
|
|
233
|
+
return new Promise((resolve) => {
|
|
234
|
+
const wake = () => {
|
|
235
|
+
clearTimeout(timer);
|
|
236
|
+
resolve(view(flow));
|
|
237
|
+
};
|
|
238
|
+
const timer = setTimeout(
|
|
239
|
+
() => {
|
|
240
|
+
flow.waiters = flow.waiters.filter((waiter) => waiter !== wake);
|
|
241
|
+
resolve(view(flow));
|
|
242
|
+
},
|
|
243
|
+
Math.min(waitMs, MAX_STATUS_WAIT_MS)
|
|
244
|
+
);
|
|
245
|
+
flow.waiters.push(wake);
|
|
246
|
+
});
|
|
247
|
+
},
|
|
151
248
|
cancel(flowId) {
|
|
152
249
|
const flow = flowOf(flowId);
|
|
153
250
|
return flow.owner.exclusive(flow.name, () => cancelFlow(flow));
|
|
@@ -158,7 +255,7 @@ export function createMcpOAuthLogins({
|
|
|
158
255
|
async complete(flowId, callbackUrl) {
|
|
159
256
|
const flow = flowOf(flowId);
|
|
160
257
|
if (flow.state !== 'pending' || flow.cancelled || !flow.awaitingCallback) throw new Error(NOT_PENDING);
|
|
161
|
-
flow.
|
|
258
|
+
flow.answer(callbackUrl);
|
|
162
259
|
await flow.done;
|
|
163
260
|
return view(flow);
|
|
164
261
|
},
|
|
@@ -140,15 +140,26 @@ function modelSwitchAnchorIndex(messages) {
|
|
|
140
140
|
* (like an inheritance boundary): the transcript shows it live and after a
|
|
141
141
|
* restore, and the model conversation gains no synthetic message. */
|
|
142
142
|
export function recordModelSwitchBoundary(session, boundary) {
|
|
143
|
+
return recordTranscriptBoundary(
|
|
144
|
+
session,
|
|
145
|
+
'modelSwitches',
|
|
146
|
+
boundary,
|
|
147
|
+
(entry) => entry?.at === boundary.at && entry?.modelId === boundary.modelId
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** A display-only boundary appended to `meta[field]` of the switch anchor
|
|
152
|
+
* message (model and workflow switches); `same` finds an already recorded one. */
|
|
153
|
+
export function recordTranscriptBoundary(session, field, boundary, same) {
|
|
143
154
|
const messages = Array.isArray(session?.messages) ? session.messages : [];
|
|
144
155
|
const index = modelSwitchAnchorIndex(messages);
|
|
145
156
|
if (index < 0) return false;
|
|
146
157
|
const anchor = messages[index];
|
|
147
|
-
const switches = Array.isArray(anchor.meta?.
|
|
148
|
-
if (switches.some(
|
|
158
|
+
const switches = Array.isArray(anchor.meta?.[field]) ? anchor.meta[field] : [];
|
|
159
|
+
if (switches.some(same)) return true;
|
|
149
160
|
// Replace the message object: the session save worker detects changed
|
|
150
161
|
// messages by reference identity, so an in-place meta edit is never saved.
|
|
151
|
-
messages[index] = { ...anchor, meta: { ...anchor.meta,
|
|
162
|
+
messages[index] = { ...anchor, meta: { ...anchor.meta, [field]: [...switches, boundary] } };
|
|
152
163
|
return true;
|
|
153
164
|
}
|
|
154
165
|
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One run of an owned automation — a schedule or a webhook endpoint. The run
|
|
3
|
+
* itself is a turn in the owner bot's home thread (routine-home-turn.mjs);
|
|
4
|
+
* the bot's reply there is its result, so nothing is posted besides it. A run
|
|
5
|
+
* whose owner bot does not exist (routine-owner.mjs) never fires: it is
|
|
6
|
+
* skipped as paused, never run elsewhere. A second run of the same automation
|
|
7
|
+
* while one is in flight is skipped (and recorded).
|
|
8
|
+
*
|
|
9
|
+
* Unattended runs (timed fires, received webhooks; not the app's or a chat's
|
|
10
|
+
* "Run now") also back off after consecutive failures (ROUTINE_LIMITS).
|
|
11
|
+
* Runs have no concurrency cap or deadline (user decision).
|
|
12
|
+
*
|
|
13
|
+
* Routine results from before routines ran in the bot's thread may still sit
|
|
14
|
+
* on a routine's record (`result_delivery`): redeliverRoutine and the daemon's
|
|
15
|
+
* recovery queue them into the owner's thread (agent-home-delivery.mjs),
|
|
16
|
+
* never running the routine again.
|
|
17
|
+
*/
|
|
18
|
+
import { announceRoutineRun, deliverScheduleResult } from './agent-home-delivery.mjs';
|
|
19
|
+
import { ROUTINE_LIMITS, routineBackoffRemaining } from '../runtime/shared/routine-limits.mjs';
|
|
20
|
+
import { ROUTINE_OWNER_PROBLEMS, routineOwnerProblem } from './routine-owner.mjs';
|
|
21
|
+
|
|
22
|
+
const DELIVERY_RETRY_MS = 60_000;
|
|
23
|
+
const errorText = (error) => error?.message || String(error);
|
|
24
|
+
const defaultLog = (line) => process.stderr.write(`mixdog routines: ${line}\n`);
|
|
25
|
+
|
|
26
|
+
// `${kind}:${name}` of every run in flight in this process: timed fires,
|
|
27
|
+
// received webhooks and the app's "Run now" all come through here.
|
|
28
|
+
const activeRuns = new Set();
|
|
29
|
+
|
|
30
|
+
// The PG record of each kind, imported on first use.
|
|
31
|
+
const RECORDS = {
|
|
32
|
+
schedule: () =>
|
|
33
|
+
import('../runtime/channels/lib/schedules-db.mjs').then((db) => ({
|
|
34
|
+
state: (name) => db.getSchedule(name),
|
|
35
|
+
list: () => db.listSchedules(),
|
|
36
|
+
recordDelivery: db.recordScheduleDelivery,
|
|
37
|
+
})),
|
|
38
|
+
webhook: () =>
|
|
39
|
+
import('../runtime/channels/lib/webhooks-db.mjs').then((db) => ({
|
|
40
|
+
state: (name) => db.loadEndpointConfig(name),
|
|
41
|
+
list: () => db.listEndpoints(),
|
|
42
|
+
recordDelivery: db.recordEndpointDelivery,
|
|
43
|
+
})),
|
|
44
|
+
};
|
|
45
|
+
/** `deps` over the kind's PG record for whatever `deps` leaves out. */
|
|
46
|
+
async function withRecord(kind, deps) {
|
|
47
|
+
if (deps.state && deps.list && deps.recordDelivery) return deps;
|
|
48
|
+
const record = await RECORDS[kind]();
|
|
49
|
+
return {
|
|
50
|
+
...deps,
|
|
51
|
+
state: deps.state || record.state,
|
|
52
|
+
list: deps.list || record.list,
|
|
53
|
+
recordDelivery: deps.recordDelivery || record.recordDelivery,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let retryArmed = false;
|
|
58
|
+
/** One timer for the whole host: re-queue every record's undelivered result. */
|
|
59
|
+
function armDeliveryRetry() {
|
|
60
|
+
if (retryArmed) return;
|
|
61
|
+
retryArmed = true;
|
|
62
|
+
setTimeout(() => {
|
|
63
|
+
retryArmed = false;
|
|
64
|
+
recoverRoutineDeliveries().catch((error) => defaultLog(`delivery retry failed: ${errorText(error)}`));
|
|
65
|
+
}, DELIVERY_RETRY_MS).unref?.();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Show `run`'s live-only row in the owner's home; a failure only logs. */
|
|
69
|
+
async function announceRun(announce, row, run, log) {
|
|
70
|
+
try {
|
|
71
|
+
await announce(row, run);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
log(`${run.kind} "${row.name}": run row not shown: ${errorText(error)}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Queue `item` for `row`'s owner; clear the record on success, mark it
|
|
78
|
+
* 'failed' (and arm the retry) when the queue refuses it. */
|
|
79
|
+
async function queueResult(row, item, entry, { deliver, recordDelivery, onDeliveryFailed, announce, log }) {
|
|
80
|
+
try {
|
|
81
|
+
await deliver(row, item);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
const message = errorText(error);
|
|
84
|
+
log(`${item.kind} "${row.name}": result could not be queued (${message}); retrying later`);
|
|
85
|
+
await recordDelivery(row.name, item.runId, { ...entry, state: 'failed', error: message }).catch((recordError) =>
|
|
86
|
+
log(`${item.kind} "${row.name}": delivery failure not recorded: ${errorText(recordError)}`)
|
|
87
|
+
);
|
|
88
|
+
// The home's run row turns into "result not delivered" (retry: retryRoutineDelivery).
|
|
89
|
+
await announceRun(announce, row, { ...item, state: 'undelivered' }, log);
|
|
90
|
+
onDeliveryFailed();
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
// A clear that fails leaves the record 'pending': the next recovery re-queues
|
|
94
|
+
// it, which the queue's run-id key turns into a no-op.
|
|
95
|
+
await recordDelivery(row.name, item.runId, null).catch((error) =>
|
|
96
|
+
log(`${item.kind} "${row.name}": delivered result not cleared: ${errorText(error)}`)
|
|
97
|
+
);
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* `row` is `{ name, ownerAgentId }`; `run({ signal })` resolves `{ sessionId,
|
|
103
|
+
* result }` (`signal` is the caller's cancellation, if any). Resolves the
|
|
104
|
+
* outcome, or `{ skipped: true, reason: 'owner' | 'overlap' | 'backoff' }`
|
|
105
|
+
* (an owner skip also names `ownerProblem` and its `message`); a failed run
|
|
106
|
+
* rejects.
|
|
107
|
+
*/
|
|
108
|
+
export async function runOwnedAutomation(kind, row, run, deps = {}) {
|
|
109
|
+
if (!row?.name) throw new Error(`${kind} run: a ${kind} row is required`);
|
|
110
|
+
const {
|
|
111
|
+
recordSkip = async () => {},
|
|
112
|
+
active = activeRuns,
|
|
113
|
+
limits = ROUTINE_LIMITS,
|
|
114
|
+
attended = false,
|
|
115
|
+
signal = null,
|
|
116
|
+
now = Date.now,
|
|
117
|
+
ownerProblem = routineOwnerProblem,
|
|
118
|
+
log = defaultLog,
|
|
119
|
+
} = deps;
|
|
120
|
+
const name = row.name;
|
|
121
|
+
const key = `${kind}:${name}`;
|
|
122
|
+
const skip = async (reason, extra = {}) => {
|
|
123
|
+
await Promise.resolve()
|
|
124
|
+
.then(() => recordSkip(name, new Date(now())))
|
|
125
|
+
.catch((error) => log(`${kind} "${name}": skip record failed: ${errorText(error)}`));
|
|
126
|
+
return { name, skipped: true, reason, sessionId: null, result: '', ...extra };
|
|
127
|
+
};
|
|
128
|
+
// A routine runs only in its own bot's thread: without that bot it waits.
|
|
129
|
+
const problem = await Promise.resolve()
|
|
130
|
+
.then(() => ownerProblem(row.ownerAgentId))
|
|
131
|
+
.catch(() => '');
|
|
132
|
+
if (problem) {
|
|
133
|
+
log(`${kind} "${name}": paused, ${ROUTINE_OWNER_PROBLEMS[problem]}`);
|
|
134
|
+
return skip('owner', { ownerProblem: problem, message: ROUTINE_OWNER_PROBLEMS[problem] });
|
|
135
|
+
}
|
|
136
|
+
if (active.has(key)) return skip('overlap');
|
|
137
|
+
active.add(key);
|
|
138
|
+
try {
|
|
139
|
+
if (!attended) {
|
|
140
|
+
const record = await withRecord(kind, deps);
|
|
141
|
+
const state = await Promise.resolve()
|
|
142
|
+
.then(() => record.state(name))
|
|
143
|
+
.catch(() => null);
|
|
144
|
+
const wait = routineBackoffRemaining(state, now(), limits);
|
|
145
|
+
if (wait > 0) {
|
|
146
|
+
log(`${kind} "${name}": skipped, backing off ${Math.ceil(wait / 1000)}s after failures in a row`);
|
|
147
|
+
return await skip('backoff');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return await run({ signal });
|
|
151
|
+
} finally {
|
|
152
|
+
active.delete(key);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Re-queue every result `row`'s record still holds, oldest first; resolves
|
|
157
|
+
* the errors of the ones the queue still refuses. Never reruns the routine. */
|
|
158
|
+
export async function redeliverRoutine(kind, row, deps = {}) {
|
|
159
|
+
const {
|
|
160
|
+
deliver = deliverScheduleResult,
|
|
161
|
+
onDeliveryFailed = armDeliveryRetry,
|
|
162
|
+
announce = announceRoutineRun,
|
|
163
|
+
log = defaultLog,
|
|
164
|
+
} = deps;
|
|
165
|
+
const record = await withRecord(kind, deps);
|
|
166
|
+
const entries = Object.entries(row?.resultDelivery || {}).sort(([, a], [, b]) =>
|
|
167
|
+
String(a?.at).localeCompare(String(b?.at))
|
|
168
|
+
);
|
|
169
|
+
const errors = [];
|
|
170
|
+
for (const [runId, entry] of entries) {
|
|
171
|
+
const result = entry?.result || {};
|
|
172
|
+
const item = {
|
|
173
|
+
runId,
|
|
174
|
+
kind,
|
|
175
|
+
ok: result.ok === true,
|
|
176
|
+
text: result.text || '',
|
|
177
|
+
runSessionId: result.runSessionId,
|
|
178
|
+
reason: result.reason,
|
|
179
|
+
paused: result.paused,
|
|
180
|
+
};
|
|
181
|
+
await queueResult(row, item, entry, {
|
|
182
|
+
...record,
|
|
183
|
+
deliver,
|
|
184
|
+
announce,
|
|
185
|
+
log,
|
|
186
|
+
onDeliveryFailed: () => errors.push(runId),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
if (errors.length) onDeliveryFailed();
|
|
190
|
+
return errors;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Daemon start (and the retry timer): re-queue every undelivered result of
|
|
194
|
+
* every schedule and webhook. Resolves how many runs still wait. Without a
|
|
195
|
+
* PostgreSQL cluster no routine exists, and none is started for this. */
|
|
196
|
+
export async function recoverRoutineDeliveries(depsByKind = null) {
|
|
197
|
+
if (!depsByKind) {
|
|
198
|
+
const [{ pgClusterExists }, { resolvePluginData }] = await Promise.all([
|
|
199
|
+
import('../runtime/memory/lib/pg/adapter.mjs'),
|
|
200
|
+
import('../runtime/shared/plugin-paths.mjs'),
|
|
201
|
+
]);
|
|
202
|
+
if (!pgClusterExists(resolvePluginData())) return 0;
|
|
203
|
+
}
|
|
204
|
+
let waiting = 0;
|
|
205
|
+
for (const kind of ['schedule', 'webhook']) {
|
|
206
|
+
const deps = await withRecord(kind, depsByKind?.[kind] || {});
|
|
207
|
+
for (const row of await deps.list()) {
|
|
208
|
+
if (row?.resultDelivery && Object.keys(row.resultDelivery).length) {
|
|
209
|
+
waiting += (await redeliverRoutine(kind, row, deps)).length;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return waiting;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** The app's "retry" on one routine: rejects with the error when still refused. */
|
|
217
|
+
export async function retryRoutineDelivery(kind, name, deps = {}) {
|
|
218
|
+
if (kind !== 'schedule' && kind !== 'webhook') throw new Error(`unknown routine kind "${kind}"`);
|
|
219
|
+
const record = await withRecord(kind, deps);
|
|
220
|
+
const row = await record.state(name);
|
|
221
|
+
if (!row) throw new Error(`${kind} "${name}" does not exist`);
|
|
222
|
+
const failed = await redeliverRoutine(kind, row, { ...deps, ...record });
|
|
223
|
+
if (failed.length) {
|
|
224
|
+
const latest = await record.state(name);
|
|
225
|
+
const error = latest?.resultDelivery?.[failed.at(-1)]?.error;
|
|
226
|
+
throw new Error(`"${name}": the result still could not be delivered${error ? ` (${error})` : ''}`);
|
|
227
|
+
}
|
|
228
|
+
return { name, delivered: Object.keys(row.resultDelivery || {}).length };
|
|
229
|
+
}
|
|
@@ -30,6 +30,11 @@ export function createResourceApi(deps) {
|
|
|
30
30
|
if (!EXTENSION_SCOPE_KINDS.includes(scopeKind)) {
|
|
31
31
|
throw new Error(`extension scope kind must be one of ${EXTENSION_SCOPE_KINDS.join(', ')}`);
|
|
32
32
|
}
|
|
33
|
+
// A connector is on in every project unless a project switches it off
|
|
34
|
+
// (setProjectConnectorEnabled); "only in this project" no longer exists.
|
|
35
|
+
if (scopeKind === 'mcp') {
|
|
36
|
+
throw new Error('MCP servers are switched off per project in Project settings (setProjectConnectorEnabled)');
|
|
37
|
+
}
|
|
33
38
|
let key = clean(name);
|
|
34
39
|
if (!key) throw new Error('extension name is required');
|
|
35
40
|
// Plugin scopes are keyed by registry id; a name must resolve to it.
|