mixdog 1.4.0 → 1.4.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/package.json +1 -1
- package/scripts/check-voice-runtime-inputs.mjs +25 -0
- package/scripts/gate-local.mjs +12 -3
- package/scripts/latest-guard.mjs +21 -9
- package/scripts/lib/isolated-test-env.mjs +10 -5
- package/scripts/lib/run-node-tests.mjs +37 -37
- package/scripts/lib/scoped-temp.d.mts +16 -0
- package/scripts/lib/scoped-temp.mjs +123 -0
- package/scripts/lib/stale-temp.mjs +44 -0
- package/scripts/post-release-check.mjs +1 -6
- package/scripts/pptx-kit-index.mjs +17 -4
- package/scripts/suite-health.mjs +10 -1
- package/scripts/test-data-dir.mjs +10 -7
- package/src/app.mjs +4 -0
- package/src/defaults/shipped-history.json +5 -0
- package/src/defaults/skills/browser-use/SKILL.md +6 -4
- package/src/defaults/skills/computer-use/SKILL.md +4 -2
- package/src/lib/rules-builder.cjs +29 -12
- package/src/output-styles/common.md +2 -2
- package/src/output-styles/simple.md +1 -1
- package/src/rules/agent/AGENT.md +5 -4
- package/src/rules/lead/LEAD.md +10 -5
- package/src/rules/shared/10-tool-workflow.md +3 -1
- package/src/rules/shared/45-destructive.md +0 -2
- package/src/rules/shared/50-execution.md +2 -0
- package/src/rules/shared/70-delivery.md +7 -9
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +29 -6
- package/src/runtime/agent/orchestrator/cache-break-trace.mjs +2 -0
- package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +1 -1
- package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +3 -1
- package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +5 -2
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-api-recovery.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +7 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-api-transport.mjs +8 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens-recovery.mjs +6 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +14 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +6 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-turn-result.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/command-code-oauth-login.mjs +1 -10
- package/src/runtime/agent/orchestrator/providers/cursor.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/devin.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +210 -20
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-prefix-trace.mjs +76 -11
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +45 -6
- package/src/runtime/agent/orchestrator/providers/lib/first-output-window.mjs +58 -0
- package/src/runtime/agent/orchestrator/providers/lib/listen-on.mjs +14 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-login.mjs +1 -10
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/chunk-events.mjs +9 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +14 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +4 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream-common.mjs +5 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +16 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events/output-items.mjs +5 -2
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events.mjs +13 -3
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +13 -2
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-watchdogs.mjs +81 -42
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +8 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +136 -5
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +20 -12
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-watchdogs.mjs +48 -22
- package/src/runtime/agent/orchestrator/providers/registry.mjs +12 -0
- package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +41 -13
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +7 -6
- package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +7 -3
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +3 -1
- package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +5 -2
- package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +1 -3
- package/src/runtime/agent/orchestrator/session/cache-warm.mjs +8 -2
- package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +1 -8
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +23 -0
- package/src/runtime/agent/orchestrator/session/compact/observation-supersession.mjs +103 -23
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +160 -36
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +13 -1
- package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +163 -22
- package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +132 -17
- package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +38 -7
- package/src/runtime/agent/orchestrator/session/conversation-import/imported-session.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +90 -22
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +33 -3
- package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +7 -2
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/provider-tool-exclusions.mjs +13 -5
- package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +13 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +39 -21
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/manager/route-fallback-chain.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +27 -9
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +61 -6
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +19 -8
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +12 -5
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-context.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/prompt-cache-touch-store.mjs +30 -7
- package/src/runtime/agent/orchestrator/session/restart-prefix-parity.fixture.mjs +154 -0
- package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +15 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +37 -8
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +9 -1
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +61 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +23 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +4 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +46 -20
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +7 -1
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +31 -5
- package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +22 -8
- package/src/runtime/agent/orchestrator/tools/builtin/lib/scoped-search-flight.mjs +9 -5
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-stream.mjs +7 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +6 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-dispatch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-publication.mjs +23 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-snapshot.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +1 -8
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/federation.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
- package/src/runtime/agent/orchestrator/tools/lib/isolated-test-env.mjs +4 -3
- package/src/runtime/agent/orchestrator/tools/lib/shell-warm-standby.mjs +26 -4
- package/src/runtime/channels/lib/webhook/signature.mjs +4 -1
- package/src/runtime/channels/lib/whisper-port.mjs +10 -1
- package/src/runtime/computer-bridge/action-schema.mjs +2 -2
- package/src/runtime/computer-bridge/core-actions.mjs +2 -2
- package/src/runtime/computer-bridge/error-recovery.mjs +5 -4
- package/src/runtime/computer-bridge/linux-action-capabilities.mjs +24 -5
- package/src/runtime/computer-bridge/linux-readiness.d.mts +7 -2
- package/src/runtime/local-provider/server-process.mjs +2 -1
- package/src/runtime/media/adapters/gemini-video.mjs +4 -1
- package/src/runtime/media/adapters/xai-media.mjs +2 -1
- package/src/runtime/memory/lib/embedding-worker.mjs +6 -3
- package/src/runtime/memory/lib/memory-action-handlers/cycle-backfill-action.mjs +1 -7
- package/src/runtime/memory/lib/memory-chunk-quality.mjs +1 -6
- package/src/runtime/office/authoring/pdf-author-action.mjs +1 -6
- package/src/runtime/office/design/design-art-direction.mjs +29 -27
- package/src/runtime/office/pdf/pdf-writer.mjs +6 -1
- package/src/runtime/office/portable/portable-snapshot-docx.mjs +7 -1
- package/src/runtime/office/portable/portable-snapshot-shared.mjs +6 -1
- package/src/runtime/office/quality/assurance-rendered.mjs +9 -2
- package/src/runtime/office/quality/assurance-structure-docx.mjs +3 -1
- package/src/runtime/office/quality/design-review.mjs +1 -5
- package/src/runtime/office/quality/document-brief.mjs +1 -5
- package/src/runtime/office/quality/forced-break-pages.mjs +9 -6
- package/src/runtime/office/quality/quality-pipeline.mjs +1 -1
- package/src/runtime/shared/bot-owned-work.mjs +56 -0
- package/src/runtime/shared/defaults-separation.mjs +1 -4
- package/src/runtime/shared/fetch-safe-listen.d.mts +16 -0
- package/src/runtime/shared/fetch-safe-listen.mjs +50 -0
- package/src/runtime/shared/handoff-artifacts.d.mts +2 -1
- package/src/runtime/shared/handoff-artifacts.mjs +18 -8
- package/src/runtime/shared/idle-auto-clear-gate.mjs +29 -7
- package/src/runtime/shared/llm/cost.mjs +5 -1
- package/src/runtime/shared/loopback-listener.mjs +25 -2
- package/src/runtime/shared/model-route-chain.d.mts +7 -2
- package/src/runtime/shared/model-route-chain.mjs +22 -4
- package/src/runtime/shared/open-url.mjs +10 -0
- package/src/runtime/shared/session-activity-signal.mjs +14 -0
- package/src/runtime/shared/session-scratch.mjs +37 -0
- package/src/runtime/shared/tool-card-model/agent-surface.mjs +1 -3
- package/src/runtime/shared/tool-card-model/card-status.mjs +15 -2
- package/src/runtime/shared/tool-result-summary.mjs +1 -1
- package/src/runtime/shared/tool-surface.mjs +3 -0
- package/src/runtime/shared/tool-work-units.mjs +2 -0
- package/src/runtime/shared/turn-snapshot-store.mjs +13 -6
- package/src/runtime/shared/worktrees/contract.mjs +4 -1
- package/src/runtime/shared/worktrees/git.mjs +15 -0
- package/src/runtime/shared/worktrees/manager.mjs +87 -38
- package/src/runtime/shared/worktrees/test-support.mjs +6 -10
- package/src/runtime/visualize-bridge/client.mjs +2 -0
- package/src/session-runtime/agent-home-delivery.mjs +41 -0
- package/src/session-runtime/agent-task-sessions.mjs +152 -63
- package/src/session-runtime/agent-tool-routing.mjs +8 -41
- package/src/session-runtime/boot/tools.mjs +5 -1
- package/src/session-runtime/bot-status.mjs +12 -6
- package/src/session-runtime/bot-target.mjs +38 -0
- package/src/session-runtime/channel-config-api.mjs +6 -1
- package/src/session-runtime/conversation-import/codex-titles.mjs +88 -0
- package/src/session-runtime/conversation-import/discovery.mjs +271 -25
- package/src/session-runtime/conversation-import/service.mjs +26 -5
- package/src/session-runtime/handoff-attachments.mjs +82 -31
- package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +12 -13
- package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +1 -0
- package/src/session-runtime/internal-tool-executor/reaction-tool.mjs +4 -1
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +8 -2
- package/src/session-runtime/lifecycle/cold-heir.mjs +11 -6
- package/src/session-runtime/lifecycle/session-catalog.mjs +9 -1
- package/src/session-runtime/model-route/set-route.mjs +11 -2
- package/src/session-runtime/provider-catalog-cache.mjs +82 -36
- package/src/session-runtime/provider-models/catalog-load.mjs +23 -6
- package/src/session-runtime/provider-models.mjs +4 -2
- package/src/session-runtime/resource-recall-api.mjs +6 -1
- package/src/session-runtime/runtime-feature-gates.mjs +5 -1
- package/src/session-runtime/services/agent-tool/execute.mjs +14 -9
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +20 -4
- package/src/session-runtime/services/agent-tool/model-select.mjs +48 -7
- package/src/session-runtime/services/agent-tool/render.mjs +71 -17
- package/src/session-runtime/services/agent-tool/spawn-preset.mjs +6 -2
- package/src/session-runtime/services/agent-tool/tool-def.mjs +9 -9
- package/src/session-runtime/services/provider-admin.mjs +2 -1
- package/src/session-runtime/tool-policy/policy-stage.mjs +1 -0
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +5 -1
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +15 -4
- package/src/session-runtime/workflow-agents-api/onboarding.mjs +3 -3
- package/src/session-runtime/workspace/transitions.mjs +25 -0
- package/src/session-runtime/workspace/worktree-tool.mjs +26 -9
- package/src/standalone/daemon-session-control.mjs +18 -2
- package/src/standalone/session-service/agent-tree/agent-turns.mjs +16 -12
- package/src/standalone/session-service/session-calls/session-action.mjs +10 -3
- package/src/standalone/session-transport/client-registry/client-grace.mjs +12 -9
- package/src/tui/app/app-view/floating-panel.jsx +7 -4
- package/src/tui/app/model-picker/route-selection/fast-parameters.mjs +1 -3
- package/src/tui/app/model-picker/route-selection.mjs +5 -6
- package/src/tui/app/provider-setup/oauth-actions.mjs +38 -20
- package/src/tui/app/provider-setup/provider-items.mjs +8 -2
- package/src/tui/app/provider-setup-picker.mjs +9 -5
- package/src/tui/components/QuestionPanel.jsx +13 -4
- package/src/tui/dist/index.mjs +75 -28
- package/src/tui/session/session-flow/queue/take-entries.mjs +11 -0
- package/src/ui/statusline-agents.mjs +1 -3
|
@@ -13,8 +13,9 @@ import { randomBytes } from 'node:crypto';
|
|
|
13
13
|
import { createOAuthPkce, parseOAuthCodeInput } from './lib/oauth-pkce.mjs';
|
|
14
14
|
import { OAUTH_PAGE_CONTENT_TYPE, oauthErrorHtml, oauthSuccessHtml } from './lib/oauth-page.mjs';
|
|
15
15
|
import { openLoginUrl } from './lib/open-login-url.mjs';
|
|
16
|
+
import { sleepWithAbort } from './retry-backoff.mjs';
|
|
16
17
|
|
|
17
|
-
const
|
|
18
|
+
const DEFAULT_ISSUER = 'https://auth.openai.com';
|
|
18
19
|
const AUTHORIZE_URL = 'https://auth.openai.com/oauth/authorize';
|
|
19
20
|
const CODEX_OAUTH_SCOPE = 'openid profile email offline_access api.connectors.read api.connectors.invoke';
|
|
20
21
|
const CALLBACK_HOST = '127.0.0.1';
|
|
@@ -23,6 +24,9 @@ const CALLBACK_PATH = '/auth/callback';
|
|
|
23
24
|
const REDIRECT_URI = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
|
|
24
25
|
const LOGIN_TIMEOUT_MS = 5 * 60_000;
|
|
25
26
|
const TOKEN_TIMEOUT_MS = 30_000;
|
|
27
|
+
// Codex device-code login: the user has 15 minutes to enter the code.
|
|
28
|
+
const DEVICE_LOGIN_TIMEOUT_MS = 15 * 60_000;
|
|
29
|
+
const DEFAULT_DEVICE_INTERVAL_S = 5;
|
|
26
30
|
|
|
27
31
|
function _scrubOAuthLoginBody(text) {
|
|
28
32
|
return String(text || '')
|
|
@@ -40,21 +44,41 @@ function _scrubOAuthLoginBody(text) {
|
|
|
40
44
|
* @param {(...tokens:string[])=>(object|null)} deps.identityFromTokens
|
|
41
45
|
* @param {(token:string)=>number} deps.expiryFromAccessToken
|
|
42
46
|
* @param {(tokens:object)=>void} deps.saveTokens
|
|
47
|
+
* @param {string} [deps.issuer] auth server base URL (tests point it at a fake server)
|
|
48
|
+
* @param {(ms:number, signal?:AbortSignal)=>Promise<void>} [deps.sleep] device-poll wait (tests inject a fast one)
|
|
49
|
+
* @param {()=>number} [deps.now]
|
|
50
|
+
* @param {number} [deps.deviceTimeoutMs] device-code login deadline (tests shorten it)
|
|
51
|
+
* @param {number} [deps.requestTimeoutMs] deadline of the device-code start request (tests shorten it)
|
|
43
52
|
*/
|
|
44
53
|
export function createOpenAIOAuthLogin(deps) {
|
|
45
|
-
const {
|
|
54
|
+
const {
|
|
55
|
+
clientId,
|
|
56
|
+
originator,
|
|
57
|
+
extractAccountId,
|
|
58
|
+
identityFromTokens,
|
|
59
|
+
expiryFromAccessToken,
|
|
60
|
+
saveTokens,
|
|
61
|
+
issuer = DEFAULT_ISSUER,
|
|
62
|
+
sleep = sleepWithAbort,
|
|
63
|
+
now = Date.now,
|
|
64
|
+
deviceTimeoutMs = DEVICE_LOGIN_TIMEOUT_MS,
|
|
65
|
+
requestTimeoutMs = TOKEN_TIMEOUT_MS,
|
|
66
|
+
} = deps;
|
|
67
|
+
const issuerBase = issuer.replace(/\/+$/, '');
|
|
68
|
+
const tokenUrl = `${issuerBase}/oauth/token`;
|
|
69
|
+
const deviceApiBase = `${issuerBase}/api/accounts`;
|
|
46
70
|
|
|
47
|
-
async function exchangeAuthorizationCode({ pkce, code, signal = null }) {
|
|
71
|
+
async function exchangeAuthorizationCode({ pkce, code, redirectUri = REDIRECT_URI, signal = null }) {
|
|
48
72
|
signal?.throwIfAborted();
|
|
49
73
|
const cleanCode = String(code || '').trim();
|
|
50
74
|
if (!cleanCode) throw new Error('[openai-oauth] authorization code is required');
|
|
51
|
-
const tokenRes = await fetch(
|
|
75
|
+
const tokenRes = await fetch(tokenUrl, {
|
|
52
76
|
method: 'POST',
|
|
53
77
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
54
78
|
body: new URLSearchParams({
|
|
55
79
|
grant_type: 'authorization_code',
|
|
56
80
|
code: cleanCode,
|
|
57
|
-
redirect_uri:
|
|
81
|
+
redirect_uri: redirectUri,
|
|
58
82
|
client_id: clientId,
|
|
59
83
|
code_verifier: pkce.verifier,
|
|
60
84
|
}),
|
|
@@ -87,7 +111,114 @@ export function createOpenAIOAuthLogin(deps) {
|
|
|
87
111
|
return tokens;
|
|
88
112
|
}
|
|
89
113
|
|
|
114
|
+
const postJson = (path, body, signal) =>
|
|
115
|
+
fetch(`${deviceApiBase}${path}`, {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
headers: { 'Content-Type': 'application/json' },
|
|
118
|
+
body: JSON.stringify(body),
|
|
119
|
+
redirect: 'error',
|
|
120
|
+
signal,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Codex device-code flow (`codex login --device-auth`): the user enters a
|
|
124
|
+
// short code at the verification URL on any machine; this host polls until
|
|
125
|
+
// the server hands back an authorization code, then exchanges it with the
|
|
126
|
+
// deviceauth redirect URI. Resolves null when the server has no device flow.
|
|
127
|
+
async function beginDeviceLogin() {
|
|
128
|
+
// No login deadline or cancel handle exists yet, so the start request
|
|
129
|
+
// carries its own timeout; it also bounds reading the response body.
|
|
130
|
+
const startSignal = AbortSignal.timeout(requestTimeoutMs);
|
|
131
|
+
const res = await postJson('/deviceauth/usercode', { client_id: clientId }, startSignal);
|
|
132
|
+
if (res.status === 404) return null;
|
|
133
|
+
if (!res.ok) throw new Error(`[openai-oauth] device code request failed with status ${res.status}`);
|
|
134
|
+
const started = await res.json();
|
|
135
|
+
const userCode = started.user_code || started.usercode;
|
|
136
|
+
const deviceAuthId = started.device_auth_id;
|
|
137
|
+
if (!userCode || !deviceAuthId)
|
|
138
|
+
throw new Error('[openai-oauth] device code response missing user_code or device_auth_id');
|
|
139
|
+
const interval = Number(String(started.interval ?? '').trim());
|
|
140
|
+
const intervalMs = (Number.isFinite(interval) && interval > 0 ? interval : DEFAULT_DEVICE_INTERVAL_S) * 1000;
|
|
141
|
+
const url = `${issuerBase}/codex/device`;
|
|
142
|
+
|
|
143
|
+
const controller = new AbortController();
|
|
144
|
+
const { signal } = controller;
|
|
145
|
+
const timedOut = () => new Error('[openai-oauth] device auth timed out after 15 minutes');
|
|
146
|
+
// One poll, aborted at the login deadline so a stalled request cannot
|
|
147
|
+
// outlive it. A reply that arrives in time is honoured whatever the clock
|
|
148
|
+
// says afterwards (the user did authorize).
|
|
149
|
+
const pollOnce = async (left) => {
|
|
150
|
+
const deadlineController = new AbortController();
|
|
151
|
+
const timer = setTimeout(() => deadlineController.abort(), left);
|
|
152
|
+
try {
|
|
153
|
+
const poll = await postJson(
|
|
154
|
+
'/deviceauth/token',
|
|
155
|
+
{ device_auth_id: deviceAuthId, user_code: userCode },
|
|
156
|
+
AbortSignal.any([signal, deadlineController.signal])
|
|
157
|
+
);
|
|
158
|
+
if (poll.ok) return { granted: await poll.json() };
|
|
159
|
+
// 403/404: the user has not approved the code yet.
|
|
160
|
+
if (poll.status !== 403 && poll.status !== 404) {
|
|
161
|
+
throw new Error(`[openai-oauth] device auth failed with status ${poll.status}`);
|
|
162
|
+
}
|
|
163
|
+
return { granted: null };
|
|
164
|
+
} catch (err) {
|
|
165
|
+
if (!signal.aborted && deadlineController.signal.aborted) throw timedOut();
|
|
166
|
+
throw err;
|
|
167
|
+
} finally {
|
|
168
|
+
clearTimeout(timer);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
const pollForCode = async () => {
|
|
172
|
+
const deadline = now() + deviceTimeoutMs;
|
|
173
|
+
for (;;) {
|
|
174
|
+
const left = deadline - now();
|
|
175
|
+
if (left <= 0) throw timedOut();
|
|
176
|
+
const { granted } = await pollOnce(left);
|
|
177
|
+
if (granted) return granted;
|
|
178
|
+
await sleep(Math.min(intervalMs, Math.max(0, deadline - now())), signal);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
const waitForCallback = (async () => {
|
|
182
|
+
try {
|
|
183
|
+
const granted = await pollForCode();
|
|
184
|
+
return await exchangeAuthorizationCode({
|
|
185
|
+
pkce: { verifier: granted.code_verifier },
|
|
186
|
+
code: granted.authorization_code,
|
|
187
|
+
redirectUri: `${issuerBase}/deviceauth/callback`,
|
|
188
|
+
signal,
|
|
189
|
+
});
|
|
190
|
+
} catch (err) {
|
|
191
|
+
// A cancelled login settles empty, as the browser flow does.
|
|
192
|
+
if (signal.aborted) return null;
|
|
193
|
+
throw err;
|
|
194
|
+
}
|
|
195
|
+
})();
|
|
196
|
+
// A cancelled or expired login may reject before a host attaches a listener.
|
|
197
|
+
waitForCallback.catch(() => {});
|
|
198
|
+
process.stderr.write(
|
|
199
|
+
`\n[openai-oauth] To log in to ChatGPT (OpenAI OAuth), open:\n${url}\nand enter this code: ${userCode}\n\n`
|
|
200
|
+
);
|
|
201
|
+
return {
|
|
202
|
+
provider: 'openai-oauth',
|
|
203
|
+
url,
|
|
204
|
+
userCode,
|
|
205
|
+
waitForCallback,
|
|
206
|
+
cancel: () => controller.abort(),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Without a browser on this host (headless/SSH) the device-code flow is used;
|
|
211
|
+
// like Codex, a server that has none (404) falls back to the browser flow.
|
|
90
212
|
async function beginOAuthLogin({ openBrowser = true } = {}) {
|
|
213
|
+
if (!openBrowser) {
|
|
214
|
+
const device = await beginDeviceLogin();
|
|
215
|
+
if (device) return device;
|
|
216
|
+
process.stderr.write('\n[openai-oauth] Device code login is not enabled; falling back to browser login.\n');
|
|
217
|
+
}
|
|
218
|
+
return await beginBrowserLogin({ openBrowser });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function beginBrowserLogin({ openBrowser }) {
|
|
91
222
|
const pkce = createOAuthPkce(64);
|
|
92
223
|
const state = randomBytes(16).toString('hex');
|
|
93
224
|
const url = new URL(AUTHORIZE_URL);
|
|
@@ -27,11 +27,22 @@ export function createWsEventSwitch({
|
|
|
27
27
|
// already cleared the pre-stream watchdog and armed the single idle timer.
|
|
28
28
|
// response.created is a MEANINGFUL frame, so it also satisfies the
|
|
29
29
|
// first-meaningful watchdog (keepalive/metadata frames never reach here,
|
|
30
|
-
// so they never clear it).
|
|
31
|
-
//
|
|
32
|
-
//
|
|
30
|
+
// so they never clear it). It is still metadata, not model output: it
|
|
31
|
+
// neither ends the first-output phase nor arms the semantic idle, and the
|
|
32
|
+
// agent sees it as transport.
|
|
33
33
|
watchdogs.clearFirstMeaningful();
|
|
34
|
-
progress('
|
|
34
|
+
progress('transport');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// A tool item starting is model output: it resets the semantic-idle timer so
|
|
38
|
+
// long server-side tool latency after item-added (before any arg delta) is
|
|
39
|
+
// not mistaken for a silent stall. An empty reasoning or message item is
|
|
40
|
+
// progress only once the model has produced output; before that the silent
|
|
41
|
+
// thinking follows it.
|
|
42
|
+
function onItemAdded() {
|
|
43
|
+
const kind = response.toolInFlight ? 'tool' : watchdogs.semanticSeen() ? 'semantic' : null;
|
|
44
|
+
if (kind) watchdogs.bumpSemantic();
|
|
45
|
+
progress(kind ?? 'transport');
|
|
35
46
|
}
|
|
36
47
|
|
|
37
48
|
function onOutputTextDelta(delta) {
|
|
@@ -84,11 +95,7 @@ export function createWsEventSwitch({
|
|
|
84
95
|
case 'response.output_item.added':
|
|
85
96
|
response.noteItemAdded(event.item);
|
|
86
97
|
reportStreamItemStart(onStreamItemStart, responsesItemStart(event.item));
|
|
87
|
-
|
|
88
|
-
// long server-side tool latency after item-added (before any arg
|
|
89
|
-
// delta) is not mistaken for a silent stall.
|
|
90
|
-
watchdogs.resetSemantic();
|
|
91
|
-
progress(response.toolInFlight ? 'tool' : 'semantic');
|
|
98
|
+
onItemAdded();
|
|
92
99
|
break;
|
|
93
100
|
case 'response.function_call_arguments.delta':
|
|
94
101
|
case 'response.custom_tool_call_input.delta':
|
|
@@ -115,9 +122,10 @@ export function createWsEventSwitch({
|
|
|
115
122
|
const kind = response.completeOutputItem(event.item);
|
|
116
123
|
// A stream that names the phase only on the finished message.
|
|
117
124
|
if (event.item?.type === 'message') reportStreamItemStart(onStreamItemStart, responsesItemStart(event.item));
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
|
|
125
|
+
// A completed item (reasoning, message, tool call) is model output —
|
|
126
|
+
// it ends the first-output phase and resets semantic idle so latency
|
|
127
|
+
// before the next item/args does not stall.
|
|
128
|
+
watchdogs.bumpSemantic();
|
|
121
129
|
progress(kind);
|
|
122
130
|
break;
|
|
123
131
|
}
|
|
@@ -24,11 +24,13 @@ import {
|
|
|
24
24
|
PROVIDER_SSE_IDLE_WATCHDOG_ENABLED,
|
|
25
25
|
PROVIDER_WS_FIRST_MEANINGFUL_TIMEOUT_MS,
|
|
26
26
|
PROVIDER_WS_SEMANTIC_IDLE_TIMEOUT_MS,
|
|
27
|
+
providerFirstOutputTimeoutMs,
|
|
27
28
|
} from '../stall-policy.mjs';
|
|
28
29
|
import { _wsErrLabel, WS_MAX_INCOMING_FRAME_BYTES } from './openai-ws-pool.mjs';
|
|
29
30
|
import { createWsResponseState } from './openai-ws-response-state.mjs';
|
|
30
31
|
import { createWsTextRelay } from './openai-ws-text-relay.mjs';
|
|
31
32
|
import { createWsStreamWatchdogs } from './openai-ws-watchdogs.mjs';
|
|
33
|
+
import { streamFirstOutputWindow } from './lib/first-output-window.mjs';
|
|
32
34
|
import {
|
|
33
35
|
enableSessionTransportTracking,
|
|
34
36
|
disableSessionTransportTracking,
|
|
@@ -85,6 +87,7 @@ function resolveStreamTimeouts(_timeouts) {
|
|
|
85
87
|
interChunkMs: _positiveInt(_timeouts?.interChunkMs, WS_INTER_CHUNK_MS),
|
|
86
88
|
maxIncomingFrameBytes: _positiveInt(_timeouts?.maxIncomingFrameBytes, WS_MAX_INCOMING_FRAME_BYTES),
|
|
87
89
|
firstMeaningfulMs: _positiveInt(_timeouts?.firstMeaningfulMs, PROVIDER_WS_FIRST_MEANINGFUL_TIMEOUT_MS),
|
|
90
|
+
firstOutputMs: _positiveInt(_timeouts?.firstOutputMs, providerFirstOutputTimeoutMs(semanticIdleMs)),
|
|
88
91
|
semanticIdleMs,
|
|
89
92
|
semanticIdleEnabled: PROVIDER_SSE_IDLE_WATCHDOG_ENABLED && semanticIdleMs > 0,
|
|
90
93
|
};
|
|
@@ -112,7 +115,8 @@ export async function _streamResponse({
|
|
|
112
115
|
if (socket?.readyState === WebSocket.OPEN) {
|
|
113
116
|
markSessionTransportActivity(state?.sessionId);
|
|
114
117
|
}
|
|
115
|
-
const { maxIncomingFrameBytes, ...timeouts } = resolveStreamTimeouts(_timeouts);
|
|
118
|
+
const { maxIncomingFrameBytes, firstOutputMs, ...timeouts } = resolveStreamTimeouts(_timeouts);
|
|
119
|
+
const firstOutput = streamFirstOutputWindow({ sessionId: state?.sessionId, onStreamDelta, timeoutMs: firstOutputMs });
|
|
116
120
|
const streamingStart = Date.now();
|
|
117
121
|
// Mid-stream retry classifier needs to distinguish "stream died before we
|
|
118
122
|
// even saw response.created" from "stream died after we had a partial
|
|
@@ -182,6 +186,7 @@ export async function _streamResponse({
|
|
|
182
186
|
streamingStart,
|
|
183
187
|
currentModel: () => response.model,
|
|
184
188
|
timeouts,
|
|
189
|
+
firstOutput,
|
|
185
190
|
attachPartialState,
|
|
186
191
|
fail,
|
|
187
192
|
});
|
|
@@ -256,6 +261,7 @@ export async function _streamResponse({
|
|
|
256
261
|
if (!handlers.attach()) return;
|
|
257
262
|
watchdogs.armPreStream();
|
|
258
263
|
watchdogs.armFirstMeaningful();
|
|
264
|
+
watchdogs.armFirstOutput();
|
|
259
265
|
// No proactive client ping: Codex's Responses WebSocket uses the library
|
|
260
266
|
// default (automatic pong replies, no periodic client ping). Incoming
|
|
261
267
|
// frames remain bounded by the 300s stream idle timeout.
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* openai-ws-watchdogs.mjs — the
|
|
2
|
+
* openai-ws-watchdogs.mjs — the five timers that bound one Responses WS
|
|
3
3
|
* stream, and what each one proves when it fires:
|
|
4
4
|
*
|
|
5
5
|
* pre-stream — no server event at all after response.create (a wedged
|
|
6
6
|
* post-upgrade socket); cleared by ANY frame.
|
|
7
7
|
* first-meaningful — no response.created / content / tool-arg delta, even
|
|
8
8
|
* while keepalive frames keep arriving; cleared once.
|
|
9
|
+
* first-output — no semantic output (reasoning, text, tool call) within
|
|
10
|
+
* the first-output window; metadata, empty items and
|
|
11
|
+
* keepalives never clear or extend it; cleared once.
|
|
9
12
|
* inter-chunk — byte silence mid-stream; reset by EVERY frame.
|
|
10
|
-
* semantic idle — deltas then silence (keepalives only);
|
|
11
|
-
*
|
|
13
|
+
* semantic idle — deltas then silence (keepalives only); armed by the
|
|
14
|
+
* first semantic output, then reset by meaningful output
|
|
15
|
+
* and item lifecycle.
|
|
12
16
|
*
|
|
13
17
|
* Every expiry hands a tagged error to `fail`, which owns terminalError, the
|
|
14
18
|
* socket close and the settle; the timers never touch the Promise.
|
|
@@ -23,6 +27,8 @@ import { streamStalledError } from '../stall-policy.mjs';
|
|
|
23
27
|
* @param {number} deps.streamingStart
|
|
24
28
|
* @param {() => string} deps.currentModel
|
|
25
29
|
* @param {{ preResponseCreatedMs: number, firstMeaningfulMs: number, interChunkMs: number, semanticIdleMs: number, semanticIdleEnabled: boolean }} deps.timeouts
|
|
30
|
+
* @param {ReturnType<typeof import('./lib/first-output-window.mjs').streamFirstOutputWindow>} deps.firstOutput
|
|
31
|
+
* the logical request's first-output window
|
|
26
32
|
* @param {(err: Error) => Error} deps.attachPartialState
|
|
27
33
|
* @param {(err: Error, closeReason: string) => void} deps.fail
|
|
28
34
|
*/
|
|
@@ -32,6 +38,7 @@ export function createWsStreamWatchdogs({
|
|
|
32
38
|
streamingStart,
|
|
33
39
|
currentModel,
|
|
34
40
|
timeouts,
|
|
41
|
+
firstOutput,
|
|
35
42
|
attachPartialState,
|
|
36
43
|
fail,
|
|
37
44
|
}) {
|
|
@@ -90,23 +97,25 @@ export function createWsStreamWatchdogs({
|
|
|
90
97
|
fail(err, 'inter_chunk_timeout');
|
|
91
98
|
}, interChunkMs);
|
|
92
99
|
};
|
|
100
|
+
// A semantic stall (first-output or idle window): partial-final recovery
|
|
101
|
+
// attaches the streamed partial state so a wedged FINAL no-tool summary can
|
|
102
|
+
// be accepted as partial-final success by the loop. pendingToolUse gates
|
|
103
|
+
// out mid-flight tools.
|
|
104
|
+
const failStalled = (event, timeoutMs) => {
|
|
105
|
+
traceWsTimeout(event, timeoutMs);
|
|
106
|
+
const err = streamStalledError('Responses WS', timeoutMs, {
|
|
107
|
+
emittedToolCall: !!midState?.emittedToolCall,
|
|
108
|
+
});
|
|
109
|
+
attachPartialState(err);
|
|
110
|
+
try {
|
|
111
|
+
err.wsCloseCode = 4000;
|
|
112
|
+
} catch {}
|
|
113
|
+
fail(err, event);
|
|
114
|
+
};
|
|
93
115
|
const resetSemanticIdle = () => {
|
|
94
116
|
if (!semanticIdleEnabled) return;
|
|
95
117
|
if (semanticIdleTimer) clearTimeout(semanticIdleTimer);
|
|
96
|
-
semanticIdleTimer = setTimeout(() =>
|
|
97
|
-
traceWsTimeout('semantic_idle_timeout', semanticIdleMs);
|
|
98
|
-
const err = streamStalledError('Responses WS', semanticIdleMs, {
|
|
99
|
-
emittedToolCall: !!midState?.emittedToolCall,
|
|
100
|
-
});
|
|
101
|
-
// Partial-final recovery: attach streamed partial state so a wedged
|
|
102
|
-
// FINAL no-tool summary can be accepted as partial-final success by
|
|
103
|
-
// the loop. pendingToolUse gates out mid-flight tools.
|
|
104
|
-
attachPartialState(err);
|
|
105
|
-
try {
|
|
106
|
-
err.wsCloseCode = 4000;
|
|
107
|
-
} catch {}
|
|
108
|
-
fail(err, 'semantic_idle_timeout');
|
|
109
|
-
}, semanticIdleMs);
|
|
118
|
+
semanticIdleTimer = setTimeout(() => failStalled('semantic_idle_timeout', semanticIdleMs), semanticIdleMs);
|
|
110
119
|
try {
|
|
111
120
|
semanticIdleTimer.unref?.();
|
|
112
121
|
} catch {}
|
|
@@ -164,24 +173,41 @@ export function createWsStreamWatchdogs({
|
|
|
164
173
|
firstMeaningfulTimer.unref?.();
|
|
165
174
|
} catch {}
|
|
166
175
|
},
|
|
176
|
+
/** Fires if the logical request produces no semantic output by its
|
|
177
|
+
* first-output deadline. Metadata (response.created / in_progress), empty
|
|
178
|
+
* item lifecycle and keepalive frames never clear or extend it, and the
|
|
179
|
+
* shorter semantic idle does not run before the first output, so a model
|
|
180
|
+
* may think silently past the idle window. A request that already had
|
|
181
|
+
* output has none: its stream runs under the semantic idle from its
|
|
182
|
+
* first frame (resetIdle). */
|
|
183
|
+
armFirstOutput() {
|
|
184
|
+
if (!firstOutput.outputSeen && semanticIdleEnabled && firstOutput.timeoutMs > 0) {
|
|
185
|
+
firstOutput.arm(() => failStalled('first_output_timeout', firstOutput.timeoutMs));
|
|
186
|
+
}
|
|
187
|
+
},
|
|
167
188
|
/** Called on EVERY parsed server event: any frame proves the socket is
|
|
168
189
|
* live. Also clears the pre-stream watchdog in case the first event is
|
|
169
|
-
* not response.created
|
|
190
|
+
* not response.created, and starts the semantic idle (once) for a
|
|
191
|
+
* request that already had output. */
|
|
170
192
|
resetIdle() {
|
|
171
193
|
clearPreStream();
|
|
172
194
|
resetInterChunk();
|
|
195
|
+
if (firstOutput.outputSeen && !semanticIdleTimer) resetSemanticIdle();
|
|
173
196
|
},
|
|
174
|
-
/** Meaningful-output progress: text/reasoning/tool
|
|
175
|
-
*
|
|
197
|
+
/** Meaningful-output progress: text/reasoning/tool output. The first one
|
|
198
|
+
* also satisfies the first-meaningful watchdog and ends the first-output
|
|
199
|
+
* phase. */
|
|
176
200
|
bumpSemantic() {
|
|
177
201
|
clearFirstMeaningful();
|
|
202
|
+
firstOutput.noteOutput();
|
|
178
203
|
resetSemanticIdle();
|
|
179
204
|
},
|
|
180
|
-
/**
|
|
181
|
-
|
|
205
|
+
/** Whether the model has produced semantic output on this stream. */
|
|
206
|
+
semanticSeen: () => firstOutput.outputSeen,
|
|
182
207
|
/** response.created is meaningful on its own. */
|
|
183
208
|
clearFirstMeaningful,
|
|
184
209
|
clearAll() {
|
|
210
|
+
firstOutput.clear();
|
|
185
211
|
if (idleTimer) clearTimeout(idleTimer);
|
|
186
212
|
if (interChunkTimer) {
|
|
187
213
|
clearTimeout(interChunkTimer);
|
|
@@ -684,6 +684,18 @@ export function refreshProviderCatalogsOnStartup() {
|
|
|
684
684
|
return _startupCatalogRefreshPromise;
|
|
685
685
|
}
|
|
686
686
|
|
|
687
|
+
// Force-refresh only the named providers' catalogs, each bounded like the full
|
|
688
|
+
// refresh. It leaves the 24h window, the catalog revision and every other
|
|
689
|
+
// provider's cache alone.
|
|
690
|
+
export function refreshProviderCatalogsFor(names) {
|
|
691
|
+
const pending = [];
|
|
692
|
+
for (const [name, provider] of availableProviderEntries()) {
|
|
693
|
+
const refreshFn = names.includes(name) ? providerCatalogRefreshFn(provider) : null;
|
|
694
|
+
if (refreshFn) pending.push(boundedCatalogRefresh(name, provider, refreshFn, 'catalog refresh'));
|
|
695
|
+
}
|
|
696
|
+
return Promise.allSettled(pending);
|
|
697
|
+
}
|
|
698
|
+
|
|
687
699
|
// Refresh the complete catalog at most once per 24h process window. The timer
|
|
688
700
|
// above calls this automatically when the window expires; callers may bypass
|
|
689
701
|
// that window after an explicit auth/config/model-refresh action.
|
|
@@ -8,9 +8,9 @@ export const ASK_USER_TOOL_DEFS = Object.freeze([
|
|
|
8
8
|
name: 'ask_user',
|
|
9
9
|
title: 'Ask User',
|
|
10
10
|
trigger:
|
|
11
|
-
'Ask the user 1–4 multiple-choice questions and wait; when a plan needs the user to pick between clear options, instead of listing them in chat.
|
|
11
|
+
'Ask the user 1–4 multiple-choice questions and wait; when a plan needs the user to pick between clear options, instead of listing them in chat.',
|
|
12
12
|
description:
|
|
13
|
-
'Ask the user multiple-choice questions and wait. Only for a decision only the user can make, with clear options; never for minor choices or approval of requested work, and open questions go in chat. Do not repeat the options in your message. A step only the user can do on a page or app (CAPTCHA, human check, sign-in, 2FA, a password)
|
|
13
|
+
'Ask the user multiple-choice questions and wait. Only for a decision only the user can make, with clear options; never for minor choices or approval of requested work, and open questions go in chat. Do not repeat the options in your message. A step only the user can do on a page or app (CAPTCHA, human check, sign-in, 2FA, a password) goes to handoff_user, never a request in plain chat. Load handoff_user directly if needed; browser/computer skills are not prerequisites.',
|
|
14
14
|
annotations: {
|
|
15
15
|
title: 'Ask User',
|
|
16
16
|
readOnlyHint: true,
|
|
@@ -52,10 +52,37 @@ export const ASK_USER_TOOL_DEFS = Object.freeze([
|
|
|
52
52
|
required: ['question', 'header', 'options'],
|
|
53
53
|
},
|
|
54
54
|
},
|
|
55
|
+
},
|
|
56
|
+
additionalProperties: false,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
// The deferred handoff tool: loaded with the browser-use and computer-use
|
|
62
|
+
// skills (dependencies.tools), the only surfaces a handoff targets.
|
|
63
|
+
export const HANDOFF_USER_TOOL_DEFS = Object.freeze([
|
|
64
|
+
{
|
|
65
|
+
name: 'handoff_user',
|
|
66
|
+
title: 'Hand Off To User',
|
|
67
|
+
trigger:
|
|
68
|
+
'Hand the user a page or app step (CAPTCHA, sign-in, 2FA, a secret); loads with the browser-use and computer-use skills.',
|
|
69
|
+
description:
|
|
70
|
+
'A step only the user can do on a page or app (CAPTCHA, human check, sign-in, 2FA, a password) is a handoff here, never a request in plain chat.',
|
|
71
|
+
annotations: {
|
|
72
|
+
title: 'Hand Off To User',
|
|
73
|
+
readOnlyHint: true,
|
|
74
|
+
destructiveHint: false,
|
|
75
|
+
idempotentHint: false,
|
|
76
|
+
openWorldHint: false,
|
|
77
|
+
agentHidden: true,
|
|
78
|
+
},
|
|
79
|
+
inputSchema: {
|
|
80
|
+
type: 'object',
|
|
81
|
+
properties: {
|
|
55
82
|
handoff: {
|
|
56
83
|
type: 'object',
|
|
57
84
|
description:
|
|
58
|
-
'
|
|
85
|
+
'secret = the user types a password, sign-in, new password or one-time code into that field of the page (you never see it; result entered/user_skipped/no_field/page_changed/blocked/expired); takeover = the user does a step and says done.',
|
|
59
86
|
properties: {
|
|
60
87
|
kind: { type: 'string', enum: ['secret', 'takeover'] },
|
|
61
88
|
surface: { type: 'string', enum: ['browser', 'computer'] },
|
|
@@ -83,26 +110,26 @@ export const ASK_USER_TOOL_DEFS = Object.freeze([
|
|
|
83
110
|
additionalProperties: false,
|
|
84
111
|
},
|
|
85
112
|
},
|
|
113
|
+
required: ['handoff'],
|
|
86
114
|
additionalProperties: false,
|
|
87
115
|
},
|
|
88
116
|
},
|
|
89
117
|
]);
|
|
90
118
|
|
|
91
|
-
/** The handoff of
|
|
92
|
-
export function
|
|
119
|
+
/** The handoff of a handoff_user call ({ kind, surface, target, field, instructions, … }). */
|
|
120
|
+
export function validateUserHandoff(args) {
|
|
93
121
|
const handoff = args?.handoff;
|
|
94
|
-
if (handoff == null)
|
|
95
|
-
if (
|
|
96
|
-
if (typeof handoff !== 'object' || Array.isArray(handoff)) throw new Error('ask_user handoff must be an object');
|
|
122
|
+
if (handoff == null) throw new Error('handoff_user needs a handoff');
|
|
123
|
+
if (typeof handoff !== 'object' || Array.isArray(handoff)) throw new Error('handoff_user handoff must be an object');
|
|
97
124
|
if (handoff.kind !== 'secret' && handoff.kind !== 'takeover') {
|
|
98
|
-
throw new Error('
|
|
125
|
+
throw new Error('handoff_user handoff kind is secret or takeover');
|
|
99
126
|
}
|
|
100
127
|
if (handoff.surface !== 'browser' && handoff.surface !== 'computer') {
|
|
101
|
-
throw new Error('
|
|
128
|
+
throw new Error('handoff_user handoff surface is browser or computer');
|
|
102
129
|
}
|
|
103
130
|
const text = (key, limit) => (typeof handoff[key] === 'string' ? handoff[key].trim().slice(0, limit) : '');
|
|
104
131
|
if (handoff.kind === 'takeover' && !text('instructions', 2000)) {
|
|
105
|
-
throw new Error('
|
|
132
|
+
throw new Error('handoff_user takeover needs instructions');
|
|
106
133
|
}
|
|
107
134
|
const login = handoffLogin(handoff);
|
|
108
135
|
let need = login ? 'sign_in' : 'action';
|
|
@@ -125,10 +152,10 @@ export function validateAskUserHandoff(args) {
|
|
|
125
152
|
function handoffLogin(handoff) {
|
|
126
153
|
if (handoff.purpose == null) return null;
|
|
127
154
|
if (handoff.purpose !== 'login' || handoff.kind !== 'takeover' || handoff.surface !== 'browser') {
|
|
128
|
-
throw new Error('
|
|
155
|
+
throw new Error('handoff_user purpose login is a browser takeover');
|
|
129
156
|
}
|
|
130
157
|
const page = loginPageUrl(handoff.url);
|
|
131
|
-
if (!page) throw new Error('
|
|
158
|
+
if (!page) throw new Error('handoff_user login needs an https url (http only on localhost)');
|
|
132
159
|
return page;
|
|
133
160
|
}
|
|
134
161
|
|
|
@@ -159,6 +186,7 @@ function handoffIdentity(handoff) {
|
|
|
159
186
|
|
|
160
187
|
/** Throws a clear error when the questions are malformed; returns the list. */
|
|
161
188
|
export function validateAskUserQuestions(args) {
|
|
189
|
+
if (args?.handoff != null) throw new Error('ask_user takes questions only; a page or app step goes to handoff_user');
|
|
162
190
|
const questions = args?.questions;
|
|
163
191
|
if (!Array.isArray(questions) || questions.length < 1 || questions.length > ASK_USER_MAX_QUESTIONS) {
|
|
164
192
|
throw new Error(`ask_user requires 1-${ASK_USER_MAX_QUESTIONS} questions`);
|
|
@@ -10,7 +10,7 @@ same change scope rather than splitting every file or step.`,
|
|
|
10
10
|
swarm: `Delegate every independent scope to its own agent; a scope is one
|
|
11
11
|
verifiable deliverable, not a bundle of steps. Treat disjoint file or
|
|
12
12
|
module sets as independent and merge scopes only on a true output
|
|
13
|
-
dependency
|
|
13
|
+
dependency.`,
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
const SOLO = `# Orchestration Mode: Solo
|
|
@@ -34,14 +34,16 @@ self-contained brief.`;
|
|
|
34
34
|
export function orchestrationInstructions(value) {
|
|
35
35
|
const mode = normalizeOrchestrationMode(value);
|
|
36
36
|
if (mode === 'none') return SOLO;
|
|
37
|
+
// Focused already says Lead executes the main scope directly.
|
|
38
|
+
const oneScope =
|
|
39
|
+
mode === 'focused' ? '' : '\nIf the plan has only one implementation scope, Lead executes it directly.';
|
|
37
40
|
return `# Orchestration Mode: ${mode[0].toUpperCase()}${mode.slice(1)}
|
|
38
41
|
|
|
39
42
|
${BATCHING[mode]}
|
|
40
43
|
|
|
41
44
|
The mode sets how work splits; an agent's description sets its role and when
|
|
42
45
|
it runs. Splitting applies to every role alike — implementation, review,
|
|
43
|
-
investigation, writing — by file, module, commit range or risk area
|
|
44
|
-
If the plan has only one implementation scope, Lead executes it directly.
|
|
46
|
+
investigation, writing — by file, module, commit range or risk area.${oneScope}
|
|
45
47
|
|
|
46
48
|
Delegation is for work whose size justifies a brief. When the brief would have
|
|
47
49
|
to spell out the edits themselves — a few localized changes whose sites Lead
|
|
@@ -49,9 +51,8 @@ already holds — Lead makes them directly in every mode; the number of files or
|
|
|
49
51
|
scopes does not change this.
|
|
50
52
|
|
|
51
53
|
Dispatch all ready independent scopes in one turn. Only a scope that requires
|
|
52
|
-
another's output waits.
|
|
53
|
-
A
|
|
54
|
-
need goes to a new agent. When scopes touch the same file, name one owner for
|
|
54
|
+
another's output waits.
|
|
55
|
+
A new independent need goes to a new agent. When scopes touch the same file, name one owner for
|
|
55
56
|
that file in each brief.
|
|
56
57
|
Use only available agents; when none fits a role, Lead does that work itself
|
|
57
58
|
and never changes settings to enable one.`;
|
|
@@ -84,12 +84,16 @@ export function cleanSessionPreview(text, max = 160) {
|
|
|
84
84
|
const limit = Math.max(16, Number(max) || 160);
|
|
85
85
|
// Previews run on truncated source: an injected block that lost its closing
|
|
86
86
|
// tag still must not become the preview.
|
|
87
|
-
|
|
87
|
+
const cleaned = stripInjectedBlocks(stripSessionDisplayEnvelope(text), { dropUnterminated: true })
|
|
88
88
|
.replace(/\[(?:Pasted text|Image)\s*#?\d+(?:\s*(?::[^\]\r\n]*|\+\d+\s+lines))?\]/gi, ' ')
|
|
89
89
|
.replace(/^Reference files:\s*/i, '')
|
|
90
90
|
.replace(/\s+/g, ' ')
|
|
91
|
-
.trim()
|
|
92
|
-
|
|
91
|
+
.trim();
|
|
92
|
+
if (cleaned.length <= limit) return cleaned;
|
|
93
|
+
// A slice of a longer string keeps that whole string alive in V8. Previews
|
|
94
|
+
// outlive their message (summary rows, catalog caches, the preview memo), so
|
|
95
|
+
// the cut is copied out: a 160-char preview must not pin the whole reply.
|
|
96
|
+
return JSON.parse(JSON.stringify(cleaned.slice(0, limit)));
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
// Stable sentinel carried in late-tool (deferred MCP) announcement reminders.
|
|
@@ -115,7 +115,7 @@ export const SESSION_TOOL = {
|
|
|
115
115
|
session_id: {
|
|
116
116
|
type: 'string',
|
|
117
117
|
description:
|
|
118
|
-
'ID from list; required for read/send/cancel/delete, optional for status/bots. send/cancel/delete cannot target the caller.',
|
|
118
|
+
'ID from list; required for read/send/cancel/delete, optional for status/bots. send/cancel/delete cannot target the caller. read/send/status also take a bot id (its home chat).',
|
|
119
119
|
},
|
|
120
120
|
prompt: { type: 'string', description: 'send: message to enqueue; loads a cold session.' },
|
|
121
121
|
to: {
|
|
@@ -264,6 +264,7 @@ export function collectStandaloneToolDefs({
|
|
|
264
264
|
visualizeToolDefs = [],
|
|
265
265
|
setupToolDefs = [],
|
|
266
266
|
askUserToolDefs = [],
|
|
267
|
+
handoffUserToolDefs = [],
|
|
267
268
|
reactionToolDefs = [],
|
|
268
269
|
goalTools = [],
|
|
269
270
|
agentTools = [],
|
|
@@ -286,6 +287,7 @@ export function collectStandaloneToolDefs({
|
|
|
286
287
|
...admitted(visualizeToolDefs, VISUALIZE_RUNTIME_TOOLS),
|
|
287
288
|
...setupToolDefs,
|
|
288
289
|
...askUserToolDefs,
|
|
290
|
+
...handoffUserToolDefs,
|
|
289
291
|
...reactionToolDefs,
|
|
290
292
|
...goalTools,
|
|
291
293
|
...agentTools,
|
|
@@ -178,8 +178,11 @@ export function webSearchRouteOrDefault(...routeLikes) {
|
|
|
178
178
|
});
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
|
|
182
|
-
|
|
181
|
+
// A stored chain may hold a fallback on the main model's own provider (inactive,
|
|
182
|
+
// see executionFallbacks), so reading and re-saving it is lenient; a route the
|
|
183
|
+
// user saves directly passes `{ strict: true }`.
|
|
184
|
+
export function normalizeWorkflowRoute(routeLike, fallback = {}, { strict = false } = {}) {
|
|
185
|
+
return normalizeRouteChain(routeLike, normalizeWorkflowRouteLeaf, fallback, { strict });
|
|
183
186
|
}
|
|
184
187
|
|
|
185
188
|
function normalizeWorkflowRouteLeaf(routeLike, fallback = {}) {
|
|
@@ -4,9 +4,7 @@
|
|
|
4
4
|
// touched path; unknown paths still fall back to a full session clear.
|
|
5
5
|
import { join, resolve as _pathResolve, isAbsolute as _pathIsAbs, normalize as _pathNorm } from 'node:path';
|
|
6
6
|
import { _normalizeCacheKey } from './util.mjs';
|
|
7
|
-
import {
|
|
8
|
-
hasGlobMagic,
|
|
9
|
-
} from '../../tools/builtin/path-utils.mjs';
|
|
7
|
+
import { hasGlobMagic } from '../../tools/builtin/path-utils.mjs';
|
|
10
8
|
import { prepareGrepArgs } from '../../tools/builtin/arg-guard.mjs';
|
|
11
9
|
import { registerCacheInvalidationListener } from '../../tools/builtin/cache-layers.mjs';
|
|
12
10
|
import { setBoundedTextCacheEntry } from './text-cache-budget.mjs';
|