@0xmaxma/claude-gateway 2.0.10 → 2.0.12
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/README.md +15 -0
- package/config.template.json +1 -1
- package/dist/agent/runner.d.ts +119 -11
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +58 -8
- package/dist/agent/runner.js.map +1 -1
- package/dist/api/browser-bindings-router.d.ts +6 -0
- package/dist/api/browser-bindings-router.d.ts.map +1 -0
- package/dist/api/browser-bindings-router.js +150 -0
- package/dist/api/browser-bindings-router.js.map +1 -0
- package/dist/api/connectors-router.js +1 -1
- package/dist/api/connectors-router.js.map +1 -1
- package/dist/api/gateway-router.d.ts.map +1 -1
- package/dist/api/gateway-router.js +30 -2
- package/dist/api/gateway-router.js.map +1 -1
- package/dist/api/jev-router.d.ts +8 -0
- package/dist/api/jev-router.d.ts.map +1 -0
- package/dist/api/jev-router.js +128 -0
- package/dist/api/jev-router.js.map +1 -0
- package/dist/api/router.d.ts.map +1 -1
- package/dist/api/router.js +53 -6
- package/dist/api/router.js.map +1 -1
- package/dist/api/voice-router.d.ts.map +1 -1
- package/dist/api/voice-router.js +42 -3
- package/dist/api/voice-router.js.map +1 -1
- package/dist/automation/browser-command.d.ts +79 -0
- package/dist/automation/browser-command.d.ts.map +1 -0
- package/dist/automation/browser-command.js +118 -0
- package/dist/automation/browser-command.js.map +1 -0
- package/dist/automation/browser-outcome.d.ts +3 -0
- package/dist/automation/browser-outcome.d.ts.map +1 -0
- package/dist/automation/browser-outcome.js +78 -0
- package/dist/automation/browser-outcome.js.map +1 -0
- package/dist/automation/browser-steps.d.ts +33 -0
- package/dist/automation/browser-steps.d.ts.map +1 -0
- package/dist/automation/browser-steps.js +293 -0
- package/dist/automation/browser-steps.js.map +1 -0
- package/dist/automation/browser-trace.d.ts +60 -0
- package/dist/automation/browser-trace.d.ts.map +1 -0
- package/dist/automation/browser-trace.js +24 -0
- package/dist/automation/browser-trace.js.map +1 -0
- package/dist/automation/browser-use.d.ts +210 -0
- package/dist/automation/browser-use.d.ts.map +1 -0
- package/dist/automation/browser-use.js +1271 -0
- package/dist/automation/browser-use.js.map +1 -0
- package/dist/automation/command-speech.d.ts +33 -0
- package/dist/automation/command-speech.d.ts.map +1 -0
- package/dist/automation/command-speech.js +117 -0
- package/dist/automation/command-speech.js.map +1 -0
- package/dist/automation/command-steps.d.ts +35 -0
- package/dist/automation/command-steps.d.ts.map +1 -0
- package/dist/automation/command-steps.js +155 -0
- package/dist/automation/command-steps.js.map +1 -0
- package/dist/automation/computer-choice-ids.d.ts +7 -0
- package/dist/automation/computer-choice-ids.d.ts.map +1 -0
- package/dist/automation/computer-choice-ids.js +24 -0
- package/dist/automation/computer-choice-ids.js.map +1 -0
- package/dist/automation/computer-command.d.ts +126 -0
- package/dist/automation/computer-command.d.ts.map +1 -0
- package/dist/automation/computer-command.js +313 -0
- package/dist/automation/computer-command.js.map +1 -0
- package/dist/automation/computer-outcome.d.ts +8 -0
- package/dist/automation/computer-outcome.d.ts.map +1 -0
- package/dist/automation/computer-outcome.js +82 -0
- package/dist/automation/computer-outcome.js.map +1 -0
- package/dist/automation/computer-policy.d.ts +84 -0
- package/dist/automation/computer-policy.d.ts.map +1 -0
- package/dist/automation/computer-policy.js +56 -0
- package/dist/automation/computer-policy.js.map +1 -0
- package/dist/automation/computer-safety.d.ts +27 -0
- package/dist/automation/computer-safety.d.ts.map +1 -0
- package/dist/automation/computer-safety.js +47 -0
- package/dist/automation/computer-safety.js.map +1 -0
- package/dist/automation/computer-steps.d.ts +20 -0
- package/dist/automation/computer-steps.d.ts.map +1 -0
- package/dist/automation/computer-steps.js +251 -0
- package/dist/automation/computer-steps.js.map +1 -0
- package/dist/automation/computer-use.d.ts +315 -0
- package/dist/automation/computer-use.d.ts.map +1 -0
- package/dist/automation/computer-use.js +864 -0
- package/dist/automation/computer-use.js.map +1 -0
- package/dist/automation/direct-command.d.ts +51 -0
- package/dist/automation/direct-command.d.ts.map +1 -0
- package/dist/automation/direct-command.js +152 -0
- package/dist/automation/direct-command.js.map +1 -0
- package/dist/automation/interrupt.d.ts +4 -0
- package/dist/automation/interrupt.d.ts.map +1 -0
- package/dist/automation/interrupt.js +26 -0
- package/dist/automation/interrupt.js.map +1 -0
- package/dist/build-provenance.json +4 -4
- package/dist/child-env-exclusions.d.ts +4 -0
- package/dist/child-env-exclusions.d.ts.map +1 -0
- package/dist/child-env-exclusions.js +23 -0
- package/dist/child-env-exclusions.js.map +1 -0
- package/dist/cli/startup-diagnostics.d.ts.map +1 -1
- package/dist/cli/startup-diagnostics.js +19 -0
- package/dist/cli/startup-diagnostics.js.map +1 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +4 -0
- package/dist/config/loader.js.map +1 -1
- package/dist/config/reload-policy.d.ts.map +1 -1
- package/dist/config/reload-policy.js +2 -1
- package/dist/config/reload-policy.js.map +1 -1
- package/dist/jev/automatic-browser-bindings.d.ts +15 -0
- package/dist/jev/automatic-browser-bindings.d.ts.map +1 -0
- package/dist/jev/automatic-browser-bindings.js +115 -0
- package/dist/jev/automatic-browser-bindings.js.map +1 -0
- package/dist/jev/browser-connector.d.ts +29 -0
- package/dist/jev/browser-connector.d.ts.map +1 -0
- package/dist/jev/browser-connector.js +347 -0
- package/dist/jev/browser-connector.js.map +1 -0
- package/dist/jev/browser-consent.d.ts +6 -0
- package/dist/jev/browser-consent.d.ts.map +1 -0
- package/dist/jev/browser-consent.js +50 -0
- package/dist/jev/browser-consent.js.map +1 -0
- package/dist/jev/browser-contract.d.ts +215 -0
- package/dist/jev/browser-contract.d.ts.map +1 -0
- package/dist/jev/browser-contract.js +10 -0
- package/dist/jev/browser-contract.js.map +1 -0
- package/dist/jev/browser-evidence-summary.d.ts +73 -0
- package/dist/jev/browser-evidence-summary.d.ts.map +1 -0
- package/dist/jev/browser-evidence-summary.js +22 -0
- package/dist/jev/browser-evidence-summary.js.map +1 -0
- package/dist/jev/browser-module-import.d.ts +3 -0
- package/dist/jev/browser-module-import.d.ts.map +1 -0
- package/dist/jev/browser-module-import.js +6 -0
- package/dist/jev/browser-module-import.js.map +1 -0
- package/dist/jev/child-env.d.ts +11 -0
- package/dist/jev/child-env.d.ts.map +1 -0
- package/dist/jev/child-env.js +60 -0
- package/dist/jev/child-env.js.map +1 -0
- package/dist/jev/computer-connector.d.ts +34 -0
- package/dist/jev/computer-connector.d.ts.map +1 -0
- package/dist/jev/computer-connector.js +82 -0
- package/dist/jev/computer-connector.js.map +1 -0
- package/dist/jev/computer-inputs.d.ts +41 -0
- package/dist/jev/computer-inputs.d.ts.map +1 -0
- package/dist/jev/computer-inputs.js +7 -0
- package/dist/jev/computer-inputs.js.map +1 -0
- package/dist/jev/index.d.ts +6 -0
- package/dist/jev/index.d.ts.map +1 -0
- package/dist/jev/index.js +25 -0
- package/dist/jev/index.js.map +1 -0
- package/dist/jev/relay-grants.d.ts +9 -0
- package/dist/jev/relay-grants.d.ts.map +1 -0
- package/dist/jev/relay-grants.js +49 -0
- package/dist/jev/relay-grants.js.map +1 -0
- package/dist/jev/service.d.ts +27 -0
- package/dist/jev/service.d.ts.map +1 -0
- package/dist/jev/service.js +264 -0
- package/dist/jev/service.js.map +1 -0
- package/dist/jev/thinking-provider.d.ts +7 -0
- package/dist/jev/thinking-provider.d.ts.map +1 -0
- package/dist/jev/thinking-provider.js +25 -0
- package/dist/jev/thinking-provider.js.map +1 -0
- package/dist/jev/types.d.ts +124 -0
- package/dist/jev/types.d.ts.map +1 -0
- package/dist/jev/types.js +13 -0
- package/dist/jev/types.js.map +1 -0
- package/dist/jev/validation.d.ts +45 -0
- package/dist/jev/validation.d.ts.map +1 -0
- package/dist/jev/validation.js +179 -0
- package/dist/jev/validation.js.map +1 -0
- package/dist/load-dotenv.d.ts.map +1 -1
- package/dist/load-dotenv.js +5 -1
- package/dist/load-dotenv.js.map +1 -1
- package/dist/orchestration/agent-tool-schemas.d.ts.map +1 -1
- package/dist/orchestration/agent-tool-schemas.js +12 -7
- package/dist/orchestration/agent-tool-schemas.js.map +1 -1
- package/dist/orchestration/background-work.d.ts +1 -0
- package/dist/orchestration/background-work.d.ts.map +1 -1
- package/dist/orchestration/background-work.js +1 -0
- package/dist/orchestration/background-work.js.map +1 -1
- package/dist/orchestration/bridge.d.ts +4 -0
- package/dist/orchestration/bridge.d.ts.map +1 -1
- package/dist/orchestration/bridge.js +176 -41
- package/dist/orchestration/bridge.js.map +1 -1
- package/dist/orchestration/browser-fields.d.ts +25 -0
- package/dist/orchestration/browser-fields.d.ts.map +1 -0
- package/dist/orchestration/browser-fields.js +23 -0
- package/dist/orchestration/browser-fields.js.map +1 -0
- package/dist/orchestration/browser-routing.d.ts +1 -1
- package/dist/orchestration/browser-routing.d.ts.map +1 -1
- package/dist/orchestration/browser-routing.js +7 -3
- package/dist/orchestration/browser-routing.js.map +1 -1
- package/dist/orchestration/capabilities.d.ts +4 -1
- package/dist/orchestration/capabilities.d.ts.map +1 -1
- package/dist/orchestration/capabilities.js +15 -9
- package/dist/orchestration/capabilities.js.map +1 -1
- package/dist/orchestration/channel-controls.js +1 -1
- package/dist/orchestration/channel-controls.js.map +1 -1
- package/dist/orchestration/cli-skills.d.ts.map +1 -1
- package/dist/orchestration/cli-skills.js +3 -2
- package/dist/orchestration/cli-skills.js.map +1 -1
- package/dist/orchestration/computer-end-screenshot.d.ts +11 -0
- package/dist/orchestration/computer-end-screenshot.d.ts.map +1 -0
- package/dist/orchestration/computer-end-screenshot.js +37 -0
- package/dist/orchestration/computer-end-screenshot.js.map +1 -0
- package/dist/orchestration/computer-prompt.d.ts +6 -0
- package/dist/orchestration/computer-prompt.d.ts.map +1 -0
- package/dist/orchestration/computer-prompt.js +24 -0
- package/dist/orchestration/computer-prompt.js.map +1 -0
- package/dist/orchestration/config.d.ts +17 -0
- package/dist/orchestration/config.d.ts.map +1 -1
- package/dist/orchestration/config.js +10 -4
- package/dist/orchestration/config.js.map +1 -1
- package/dist/orchestration/container-tool-schemas.d.ts +47 -3
- package/dist/orchestration/container-tool-schemas.d.ts.map +1 -1
- package/dist/orchestration/container-tool-schemas.js +41 -6
- package/dist/orchestration/container-tool-schemas.js.map +1 -1
- package/dist/orchestration/container.d.ts +4 -1
- package/dist/orchestration/container.d.ts.map +1 -1
- package/dist/orchestration/container.js +48 -29
- package/dist/orchestration/container.js.map +1 -1
- package/dist/orchestration/context-footprint.d.ts +1 -1
- package/dist/orchestration/context-footprint.d.ts.map +1 -1
- package/dist/orchestration/context-footprint.js +5 -5
- package/dist/orchestration/context-footprint.js.map +1 -1
- package/dist/orchestration/control-notification.d.ts +6 -0
- package/dist/orchestration/control-notification.d.ts.map +1 -0
- package/dist/orchestration/control-notification.js +23 -0
- package/dist/orchestration/control-notification.js.map +1 -0
- package/dist/orchestration/dashboard-reader-worker.js +2 -2
- package/dist/orchestration/dashboard-reader-worker.js.map +1 -1
- package/dist/orchestration/execution-observation.d.ts +3 -0
- package/dist/orchestration/execution-observation.d.ts.map +1 -1
- package/dist/orchestration/execution-observation.js.map +1 -1
- package/dist/orchestration/gateway-tasks/browser.d.ts +59 -0
- package/dist/orchestration/gateway-tasks/browser.d.ts.map +1 -0
- package/dist/orchestration/gateway-tasks/browser.js +426 -0
- package/dist/orchestration/gateway-tasks/browser.js.map +1 -0
- package/dist/orchestration/gateway-tasks/computer-context.d.ts +6 -0
- package/dist/orchestration/gateway-tasks/computer-context.d.ts.map +1 -0
- package/dist/orchestration/gateway-tasks/computer-context.js +19 -0
- package/dist/orchestration/gateway-tasks/computer-context.js.map +1 -0
- package/dist/orchestration/gateway-tasks/computer.d.ts +123 -0
- package/dist/orchestration/gateway-tasks/computer.d.ts.map +1 -0
- package/dist/orchestration/gateway-tasks/computer.js +519 -0
- package/dist/orchestration/gateway-tasks/computer.js.map +1 -0
- package/dist/orchestration/gateway-tasks/controller.d.ts +40 -6
- package/dist/orchestration/gateway-tasks/controller.d.ts.map +1 -1
- package/dist/orchestration/gateway-tasks/controller.js +118 -12
- package/dist/orchestration/gateway-tasks/controller.js.map +1 -1
- package/dist/orchestration/gateway-tasks/safemode.d.ts.map +1 -1
- package/dist/orchestration/gateway-tasks/safemode.js +2 -1
- package/dist/orchestration/gateway-tasks/safemode.js.map +1 -1
- package/dist/orchestration/jev-gateway.d.ts +25 -0
- package/dist/orchestration/jev-gateway.d.ts.map +1 -0
- package/dist/orchestration/jev-gateway.js +103 -0
- package/dist/orchestration/jev-gateway.js.map +1 -0
- package/dist/orchestration/jev-tool.d.ts +45 -0
- package/dist/orchestration/jev-tool.d.ts.map +1 -0
- package/dist/orchestration/jev-tool.js +10 -0
- package/dist/orchestration/jev-tool.js.map +1 -0
- package/dist/orchestration/live-execution-input.d.ts +26 -0
- package/dist/orchestration/live-execution-input.d.ts.map +1 -0
- package/dist/orchestration/live-execution-input.js +70 -0
- package/dist/orchestration/live-execution-input.js.map +1 -0
- package/dist/orchestration/notification-mailbox.d.ts.map +1 -1
- package/dist/orchestration/notification-mailbox.js +5 -2
- package/dist/orchestration/notification-mailbox.js.map +1 -1
- package/dist/orchestration/process-platform.d.ts +0 -3
- package/dist/orchestration/process-platform.d.ts.map +1 -1
- package/dist/orchestration/process-platform.js +0 -4
- package/dist/orchestration/process-platform.js.map +1 -1
- package/dist/orchestration/process-turn.d.ts +8 -0
- package/dist/orchestration/process-turn.d.ts.map +1 -1
- package/dist/orchestration/process-turn.js +65 -8
- package/dist/orchestration/process-turn.js.map +1 -1
- package/dist/orchestration/response-errors.d.ts.map +1 -1
- package/dist/orchestration/response-errors.js +1 -0
- package/dist/orchestration/response-errors.js.map +1 -1
- package/dist/orchestration/runtime.d.ts +51 -1
- package/dist/orchestration/runtime.d.ts.map +1 -1
- package/dist/orchestration/runtime.js +383 -27
- package/dist/orchestration/runtime.js.map +1 -1
- package/dist/orchestration/skill-catalog-budget.d.ts +6 -0
- package/dist/orchestration/skill-catalog-budget.d.ts.map +1 -0
- package/dist/orchestration/skill-catalog-budget.js +9 -0
- package/dist/orchestration/skill-catalog-budget.js.map +1 -0
- package/dist/orchestration/skills.d.ts +4 -2
- package/dist/orchestration/skills.d.ts.map +1 -1
- package/dist/orchestration/skills.js +44 -3
- package/dist/orchestration/skills.js.map +1 -1
- package/dist/orchestration/store.d.ts +1 -0
- package/dist/orchestration/store.d.ts.map +1 -1
- package/dist/orchestration/store.js +2 -0
- package/dist/orchestration/store.js.map +1 -1
- package/dist/orchestration/task-controls.d.ts +78 -8
- package/dist/orchestration/task-controls.d.ts.map +1 -1
- package/dist/orchestration/task-controls.js +6 -1
- package/dist/orchestration/task-controls.js.map +1 -1
- package/dist/orchestration/tasks/automation-session.d.ts +25 -0
- package/dist/orchestration/tasks/automation-session.d.ts.map +1 -0
- package/dist/orchestration/tasks/automation-session.js +68 -0
- package/dist/orchestration/tasks/automation-session.js.map +1 -0
- package/dist/orchestration/tasks/driver.d.ts.map +1 -1
- package/dist/orchestration/tasks/driver.js +3 -2
- package/dist/orchestration/tasks/driver.js.map +1 -1
- package/dist/orchestration/tasks/service.d.ts +68 -7
- package/dist/orchestration/tasks/service.d.ts.map +1 -1
- package/dist/orchestration/tasks/service.js +646 -44
- package/dist/orchestration/tasks/service.js.map +1 -1
- package/dist/orchestration/types.d.ts +82 -3
- package/dist/orchestration/types.d.ts.map +1 -1
- package/dist/orchestration/types.js.map +1 -1
- package/dist/safemode/context.d.ts.map +1 -1
- package/dist/safemode/context.js +169 -72
- package/dist/safemode/context.js.map +1 -1
- package/dist/safemode/external-owners.d.ts +4 -1
- package/dist/safemode/external-owners.d.ts.map +1 -1
- package/dist/safemode/external-owners.js +36 -9
- package/dist/safemode/external-owners.js.map +1 -1
- package/dist/safemode/native.d.ts.map +1 -1
- package/dist/safemode/native.js +16 -10
- package/dist/safemode/native.js.map +1 -1
- package/dist/session/codex-extension-discovery.d.ts.map +1 -1
- package/dist/session/codex-extension-discovery.js +4 -2
- package/dist/session/codex-extension-discovery.js.map +1 -1
- package/dist/session/codex-native-mcp-client.d.ts.map +1 -1
- package/dist/session/codex-native-mcp-client.js +2 -1
- package/dist/session/codex-native-mcp-client.js.map +1 -1
- package/dist/session/codex-process.d.ts.map +1 -1
- package/dist/session/codex-process.js +9 -4
- package/dist/session/codex-process.js.map +1 -1
- package/dist/session/process.d.ts +16 -4
- package/dist/session/process.d.ts.map +1 -1
- package/dist/session/process.js +153 -75
- package/dist/session/process.js.map +1 -1
- package/dist/session/runtime-profile.d.ts +10 -5
- package/dist/session/runtime-profile.d.ts.map +1 -1
- package/dist/session/runtime-profile.js +5 -5
- package/dist/session/runtime-profile.js.map +1 -1
- package/dist/session/worker-extensions.d.ts.map +1 -1
- package/dist/session/worker-extensions.js +2 -1
- package/dist/session/worker-extensions.js.map +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/dashboard-client.d.ts.map +1 -1
- package/dist/ui/dashboard-client.js +33 -3
- package/dist/ui/dashboard-client.js.map +1 -1
- package/dist/ui/web-ui.js +1 -1
- package/lib/automation/browser-thinking.cjs +9 -0
- package/lib/automation/browser-thinking.d.cts +2 -0
- package/lib/automation/computer-thinking.cjs +10 -0
- package/lib/automation/computer-thinking.d.cts +2 -0
- package/lib/automation/index.cjs +47 -0
- package/lib/automation/index.d.cts +3 -0
- package/lib/automation/reasoning-instructions.cjs +5 -0
- package/lib/automation/thinking.cjs +35 -0
- package/lib/automation/thinking.d.cts +2 -0
- package/mcp/capability-catalog.ts +2 -2
- package/mcp/server.ts +1 -1
- package/mcp/tools/tasks/module.ts +9 -0
- package/package.json +5 -3
- package/scripts/orchestration/smoke-safemode-codex.cjs +1 -1
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TaskService = void 0;
|
|
4
|
+
exports.handoffCommandId = handoffCommandId;
|
|
4
5
|
exports.taskIndexEntry = taskIndexEntry;
|
|
6
|
+
const control_notification_1 = require("../control-notification");
|
|
7
|
+
const browser_fields_1 = require("../browser-fields");
|
|
8
|
+
const computer_inputs_1 = require("../../jev/computer-inputs");
|
|
9
|
+
const automation_session_1 = require("./automation-session");
|
|
10
|
+
const computer_outcome_1 = require("../../automation/computer-outcome");
|
|
11
|
+
const browser_outcome_1 = require("../../automation/browser-outcome");
|
|
12
|
+
const direct_command_1 = require("../../automation/direct-command");
|
|
13
|
+
const browser_contract_1 = require("../../jev/browser-contract");
|
|
5
14
|
const path_1 = require("path");
|
|
6
15
|
const workflow_1 = require("../workflow");
|
|
7
16
|
const timing_1 = require("./timing");
|
|
@@ -18,6 +27,13 @@ const types_1 = require("../types");
|
|
|
18
27
|
/** How many finished tasks the per-turn index page keeps. Unfinished tasks are never dropped;
|
|
19
28
|
* older finished ones stay reachable through task_status with an explicit task_id. */
|
|
20
29
|
const INDEXED_FINISHED_TASKS = 20;
|
|
30
|
+
/** Direct Computer/Remote Browser commands that may wait behind a settling round. */
|
|
31
|
+
const COMMAND_QUEUE_MAX = 20;
|
|
32
|
+
/** Stable control ID for the agent's one command on a handed-off utterance. */
|
|
33
|
+
function handoffCommandId(inputId) {
|
|
34
|
+
const h = (0, crypto_1.createHash)('sha256').update('agent-handoff:' + inputId).digest('hex');
|
|
35
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
|
|
36
|
+
}
|
|
21
37
|
/** One index row: identity, live state and how to fetch the rest — never a report body.
|
|
22
38
|
* A finished task's progress, execution observation and workflow checkpoint/review history
|
|
23
39
|
* are report bodies, so they are dropped here and read back via task_status when needed.
|
|
@@ -35,10 +51,36 @@ function taskIndexEntry(task) {
|
|
|
35
51
|
cancellation: task.cancellation, replacedByTaskId: task.replacedByTaskId,
|
|
36
52
|
workstreamId: task.workstreamId, continueTaskId: task.continueTaskId, continuationPolicy: task.continuationPolicy,
|
|
37
53
|
resultAvailable: Boolean(task.result),
|
|
38
|
-
|
|
54
|
+
actionLog: task.actionLog,
|
|
55
|
+
computerConnection: task.computerConnection, automationController: task.automationController ?? "agent", gatewayTarget: task.gatewayTarget, automationSession: (0, automation_session_1.automationSession)(task),
|
|
39
56
|
details: { tool: 'task_status', task_id: task.taskId },
|
|
40
57
|
};
|
|
41
58
|
}
|
|
59
|
+
const ACTION_LOG_MAX = 12;
|
|
60
|
+
const CORRECTION_HEAD = 'Latest user correction (apply first; supersedes conflicting earlier requirements):\n';
|
|
61
|
+
/** The round's own command for the log: a correction's latest text, never its preamble, and never typed field text. */
|
|
62
|
+
function loggedCommand(instructions) {
|
|
63
|
+
let command = instructions.startsWith(CORRECTION_HEAD) ? instructions.slice(CORRECTION_HEAD.length).split('\n\nEarlier requirements and corrections')[0] : instructions;
|
|
64
|
+
const typed = (0, direct_command_1.textCommand)(command);
|
|
65
|
+
if (typed)
|
|
66
|
+
command = command.replace(typed.text, '[text]');
|
|
67
|
+
return command;
|
|
68
|
+
}
|
|
69
|
+
const clip = (text, max) => { const chars = [...text.replace(/\s+/gu, ' ').trim()]; return chars.length > max ? chars.slice(0, max - 1).join('') + '…' : chars.join(''); };
|
|
70
|
+
/** Appends one round's own outcome line, so a later summary need not infer earlier rounds from the latest report. */
|
|
71
|
+
function recordAction(task, revision, command, outcome) {
|
|
72
|
+
let result = task.gatewayTarget?.adapter === 'computer' && outcome.computerReport ? (0, computer_outcome_1.computerOutcomeText)(outcome.computerReport)
|
|
73
|
+
: task.gatewayTarget?.adapter === 'browser' && outcome.browserReport ? (0, browser_outcome_1.browserOutcomeText)(outcome.browserReport) : undefined;
|
|
74
|
+
if (result === undefined)
|
|
75
|
+
return;
|
|
76
|
+
const last = outcome.computerReport?.lastAction;
|
|
77
|
+
// A step-run line counts steps; name the last completed action as well.
|
|
78
|
+
if (last && !last.blocked && !result.startsWith('Done:'))
|
|
79
|
+
result += ` Last completed action: ${(0, computer_outcome_1.computerActionText)(last)}.`;
|
|
80
|
+
if (outcome.type === 'unknown')
|
|
81
|
+
result = 'Outcome unknown; not replayed. ' + result;
|
|
82
|
+
task.actionLog = [...(task.actionLog ?? []).filter(entry => entry.revision !== revision), { revision, command: clip(loggedCommand(command), 80), result: clip(result, 240) }].slice(-ACTION_LOG_MAX);
|
|
83
|
+
}
|
|
42
84
|
/** Task mutations use short durable transactions; admission probes are read-only. */
|
|
43
85
|
class TaskService {
|
|
44
86
|
constructor(store, config, defaultProjectRoot = '') {
|
|
@@ -124,7 +166,7 @@ class TaskService {
|
|
|
124
166
|
delete task.skill;
|
|
125
167
|
return [this.withRecentTools(task)];
|
|
126
168
|
}
|
|
127
|
-
return this.store.all(`SELECT snapshot_json FROM tasks WHERE conversation_id=? AND (state NOT IN ('completed','failed','cancelled') OR id IN
|
|
169
|
+
return this.store.all(`SELECT snapshot_json FROM tasks WHERE conversation_id=? AND (state NOT IN ('completed','failed','cancelled') OR (json_extract(snapshot_json,'$.automationSession.status') IN ('idle','blocked') AND json_extract(snapshot_json,'$.automationSession.idleSince')+json_extract(snapshot_json,'$.automationSession.idleTimeoutMs')>${Date.now()}) OR id IN
|
|
128
170
|
(SELECT id FROM tasks WHERE conversation_id=? AND state IN ('completed','failed','cancelled') ORDER BY created_at DESC,id DESC LIMIT 100))
|
|
129
171
|
ORDER BY CASE WHEN state IN ('completed','failed','cancelled') THEN 1 ELSE 0 END,created_at DESC,id DESC`, conversationId, conversationId).map(row => {
|
|
130
172
|
const task = JSON.parse(String(row.snapshot_json));
|
|
@@ -155,11 +197,12 @@ class TaskService {
|
|
|
155
197
|
}
|
|
156
198
|
/** Bounded recent-task page: every unfinished task, plus only the newest finished ones. */
|
|
157
199
|
indexPage(conversationId) {
|
|
158
|
-
return this.store.all(`SELECT snapshot_json FROM tasks WHERE conversation_id=? AND (state NOT IN ('completed','failed','cancelled') OR id IN
|
|
200
|
+
return this.store.all(`SELECT snapshot_json FROM tasks WHERE conversation_id=? AND (state NOT IN ('completed','failed','cancelled') OR (json_extract(snapshot_json,'$.automationSession.status') IN ('idle','blocked') AND json_extract(snapshot_json,'$.automationSession.idleSince')+json_extract(snapshot_json,'$.automationSession.idleTimeoutMs')>${Date.now()}) OR id IN
|
|
159
201
|
(SELECT id FROM tasks WHERE conversation_id=? AND state IN ('completed','failed','cancelled') ORDER BY created_at DESC,id DESC LIMIT ${INDEXED_FINISHED_TASKS}))
|
|
160
202
|
ORDER BY CASE WHEN state IN ('completed','failed','cancelled') THEN 1 ELSE 0 END,created_at DESC,id DESC`, conversationId, conversationId).map(row => JSON.parse(String(row.snapshot_json)));
|
|
161
203
|
}
|
|
162
204
|
withRecentTools(task) {
|
|
205
|
+
task.automationSession = (0, automation_session_1.automationSession)(task);
|
|
163
206
|
task.recentTools = this.store.all("SELECT payload_json,occurred_at FROM conversation_events WHERE conversation_id=? AND type='tool.activity' AND json_extract(payload_json,'$.task_id')=? ORDER BY seq DESC LIMIT 8", task.conversationId, task.taskId).map(row => {
|
|
164
207
|
const event = JSON.parse(String(row.payload_json)).payload;
|
|
165
208
|
return { name: String(event.name ?? 'unknown'), description: typeof event.input?.description === 'string' ? (0, failure_1.taskFailure)(new Error(event.input.description)).message.slice(0, 512) : undefined, type: String(event.type), isError: event.is_error, occurredAt: Number(row.occurred_at) };
|
|
@@ -171,9 +214,18 @@ class TaskService {
|
|
|
171
214
|
.map(row => String(row.path).slice(0, 2048)),
|
|
172
215
|
currentFilesystemVerified: false,
|
|
173
216
|
};
|
|
174
|
-
|
|
217
|
+
// Direct corrections bypass the parent inference turn. Hydrated status and
|
|
218
|
+
// notifications must carry the current goal so verification cannot use stale memory.
|
|
219
|
+
return ['browser', 'computer'].includes(task.gatewayTarget?.adapter ?? '')
|
|
220
|
+
? { ...task, currentInstructions: this.revision(task.taskId, task.revision).instructions }
|
|
221
|
+
: task;
|
|
175
222
|
}
|
|
176
223
|
spawn(context, command) {
|
|
224
|
+
if (command.computerInputs !== undefined) {
|
|
225
|
+
command.computerInputs = computer_inputs_1.ComputerInputs.parse(command.computerInputs);
|
|
226
|
+
if (command.gatewayTarget?.adapter !== 'computer')
|
|
227
|
+
throw new types_1.OrchestrationError('INVALID_INPUT');
|
|
228
|
+
}
|
|
177
229
|
if (command.workingDirectory !== undefined) {
|
|
178
230
|
if (this.config.tasks.workspaceMode !== 'host')
|
|
179
231
|
throw new types_1.OrchestrationError('WORKING_DIRECTORY_HOST_ONLY');
|
|
@@ -184,6 +236,9 @@ class TaskService {
|
|
|
184
236
|
(0, store_1.boundedText)(command.continueTaskId, 128);
|
|
185
237
|
if (command.continuationPolicy !== undefined && (!command.continueTaskId || !['after_success', 'after_terminal'].includes(command.continuationPolicy)))
|
|
186
238
|
throw new types_1.OrchestrationError('INVALID_INPUT');
|
|
239
|
+
const prepared = (0, browser_fields_1.preparedBrowserAnswers)(command.browserFields, context.inputId);
|
|
240
|
+
if (command.browserFields !== undefined && command.gatewayTarget?.adapter !== 'browser')
|
|
241
|
+
throw new types_1.OrchestrationError('INVALID_BROWSER_FIELDS');
|
|
187
242
|
(0, store_1.boundedText)(command.title, 512);
|
|
188
243
|
(0, store_1.boundedText)(command.instructions);
|
|
189
244
|
(0, store_1.boundedText)(command.targetProfile, 128);
|
|
@@ -197,6 +252,24 @@ class TaskService {
|
|
|
197
252
|
throw new types_1.OrchestrationError('INVALID_GATEWAY_TARGET');
|
|
198
253
|
return this.command(context, 'spawn', command, true, () => {
|
|
199
254
|
const conversation = this.store.get('SELECT * FROM conversations WHERE id=?', context.conversationId);
|
|
255
|
+
let replaced;
|
|
256
|
+
if (['browser', 'computer'].includes(command.gatewayTarget?.adapter ?? '')) {
|
|
257
|
+
const existing = this.store.all("SELECT snapshot_json FROM tasks WHERE conversation_id=? AND json_extract(snapshot_json,'$.gatewayTarget.adapter')=? AND json_extract(snapshot_json,'$.gatewayTarget.sessionId')=?", context.conversationId, command.gatewayTarget.adapter, command.gatewayTarget.sessionId).map(row => JSON.parse(String(row.snapshot_json))).find(task => task.ownerPrincipalId === context.principalId && (0, automation_session_1.automationSession)(task)?.status !== 'closed');
|
|
258
|
+
// A computer session that never acted (failed before or while waiting
|
|
259
|
+
// for access) holds nothing worth continuing; a new spawn replaces it.
|
|
260
|
+
const neverActed = (existing?.gatewayTarget?.adapter === 'computer' && (existing.computerReport?.steps ?? 0) === 0 && !existing.computerReport?.trace?.some(event => event.outcome === 'unknown') &&
|
|
261
|
+
((existing.state === 'failed' && !existing.activeAttemptId) || (existing.state === 'running' && existing.computerReport?.status === 'waiting_access'))) ||
|
|
262
|
+
// A Remote Browser session that failed before any tab action likewise holds nothing.
|
|
263
|
+
(existing?.gatewayTarget?.adapter === 'browser' && existing.state === 'failed' && !existing.activeAttemptId && (existing.browserReport?.steps ?? 0) === 0 &&
|
|
264
|
+
existing.browserReport?.lastAction?.outcome !== 'unknown' && !existing.browserReport?.lastConfirmedAction);
|
|
265
|
+
if (existing && neverActed)
|
|
266
|
+
replaced = existing;
|
|
267
|
+
else if (existing) {
|
|
268
|
+
const report = existing.computerReport ?? existing.browserReport;
|
|
269
|
+
const cause = `state ${existing.state}${report?.reason ? `, last result ${report.reason}` : ''}${existing.failure?.code ? ` (${existing.failure.code})` : ''}, controlled by the ${existing.automationController === 'user' ? 'user' : 'agent'}`;
|
|
270
|
+
throw new types_1.OrchestrationError('AUTOMATION_SESSION_EXISTS', `This device session already has task ${existing.taskId} at revision ${existing.revision}: ${cause}. This is not a permission problem. Inspect it with task_status and use task_update when_ready for the next goal on the same task${existing.automationController === 'user' ? ' (the user controls it; wait for them or ask them to hand control back)' : ''}, or task_cancel it to end that session. Do not open another tab or replay completed work.`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
200
273
|
const available = new Set();
|
|
201
274
|
for (const input of this.store.all('SELECT id,attachment_refs_json FROM conversation_inputs WHERE conversation_id=?', context.conversationId)) {
|
|
202
275
|
available.add(String(input.id));
|
|
@@ -231,8 +304,13 @@ class TaskService {
|
|
|
231
304
|
task.continuationPolicy = command.continuationPolicy ?? 'after_success';
|
|
232
305
|
task.latestProgress = { source: 'runtime', observedAt: now, text: `Queued after task ${prior.taskId} (${task.continuationPolicy}).` };
|
|
233
306
|
}
|
|
234
|
-
if (command.gatewayTarget)
|
|
307
|
+
if (command.gatewayTarget) {
|
|
235
308
|
task.gatewayTarget = command.gatewayTarget;
|
|
309
|
+
if (["browser", "computer"].includes(command.gatewayTarget.adapter)) {
|
|
310
|
+
task.automationSession = { status: "active", idleTimeoutMs: this.config.tasks.automationIdleTimeoutMs };
|
|
311
|
+
task.automationController = "user";
|
|
312
|
+
}
|
|
313
|
+
}
|
|
236
314
|
if (command.skill)
|
|
237
315
|
task.skill = command.skill;
|
|
238
316
|
if (context.model && !command.gatewayTarget)
|
|
@@ -241,30 +319,104 @@ class TaskService {
|
|
|
241
319
|
if (projectRoot && !command.gatewayTarget)
|
|
242
320
|
task.resourceProfile = { projectRoot, mode: this.config.tasks.workspaceMode };
|
|
243
321
|
this.store.run('INSERT INTO tasks VALUES(?,?,?,?,?,?,?,?,?)', task.taskId, task.conversationId, task.state, 1, 1, null, JSON.stringify(task), now, now);
|
|
244
|
-
const revision = { taskId: task.taskId, revision: 1, instructions: command.instructions,
|
|
245
|
-
contextRefs: command.contextRefs ?? [], mode: 'when_ready', originatingInputId: context.inputId };
|
|
322
|
+
const revision = { requestBrowserConsent: command.gatewayTarget?.adapter === 'browser', taskId: task.taskId, revision: 1, instructions: command.instructions, computerInputs: command.computerInputs,
|
|
323
|
+
answers: prepared, contextRefs: command.contextRefs ?? [], mode: 'when_ready', originatingInputId: context.inputId };
|
|
246
324
|
this.store.run('INSERT INTO task_revisions VALUES(?,?,?)', task.taskId, 1, JSON.stringify(revision));
|
|
247
325
|
this.store.appendEvent(task.conversationId, 'task.created', task, task.taskId);
|
|
248
326
|
this.store.enqueue('schedule', `schedule:${task.taskId}:1`, { taskId: task.taskId });
|
|
327
|
+
if (replaced) {
|
|
328
|
+
this.cancelOwned(context.conversationId, replaced.taskId, task.taskId);
|
|
329
|
+
this.store.appendEvent(task.conversationId, 'task.session_replaced', { taskId: replaced.taskId, replacedByTaskId: task.taskId }, replaced.taskId);
|
|
330
|
+
}
|
|
249
331
|
return task;
|
|
250
332
|
});
|
|
251
333
|
}
|
|
252
|
-
update(context, taskId, expectedRevision, instruction, mode) {
|
|
334
|
+
update(context, taskId, expectedRevision, instruction, mode, browserFields, computerInputs, startUrl) {
|
|
335
|
+
let browserUrl;
|
|
336
|
+
if (startUrl !== undefined) {
|
|
337
|
+
if (!context.execute)
|
|
338
|
+
throw new types_1.OrchestrationError('EXECUTION_DENIED');
|
|
339
|
+
try {
|
|
340
|
+
if (typeof startUrl !== 'string' || startUrl.length > 8192)
|
|
341
|
+
throw Error();
|
|
342
|
+
const u = new URL(startUrl);
|
|
343
|
+
if (!['http:', 'https:'].includes(u.protocol) || u.username || u.password)
|
|
344
|
+
throw Error();
|
|
345
|
+
browserUrl = u.href;
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
throw new types_1.OrchestrationError('INVALID_BROWSER_START_URL');
|
|
349
|
+
}
|
|
350
|
+
}
|
|
253
351
|
(0, store_1.boundedText)(instruction);
|
|
352
|
+
if (computerInputs !== undefined)
|
|
353
|
+
computerInputs = computer_inputs_1.ComputerInputs.parse(computerInputs);
|
|
254
354
|
if (!['when_ready', 'interrupt_and_resume'].includes(mode))
|
|
255
355
|
throw new types_1.OrchestrationError('INVALID_INPUT');
|
|
256
|
-
|
|
356
|
+
const prepared = (0, browser_fields_1.preparedBrowserAnswers)(browserFields, context.inputId);
|
|
357
|
+
return this.command(context, 'update', { taskId, expectedRevision, instruction, mode, browserFields, computerInputs, startUrl }, context.execute, () => {
|
|
257
358
|
const task = this.owned(taskId, context.conversationId), version = task.stateVersion;
|
|
258
|
-
if (
|
|
359
|
+
if (browserUrl !== undefined && task.gatewayTarget?.adapter !== 'browser')
|
|
360
|
+
throw new types_1.OrchestrationError('INVALID_BROWSER_START_URL');
|
|
361
|
+
if (computerInputs !== undefined && task.gatewayTarget?.adapter !== 'computer')
|
|
362
|
+
throw new types_1.OrchestrationError('INVALID_INPUT');
|
|
363
|
+
if ((0, automation_session_1.automationSession)(task)?.status === 'closed')
|
|
364
|
+
throw new types_1.OrchestrationError('AUTOMATION_SESSION_CLOSED', 'This automation session was ended or expired. Ask for a new authorized session; never replay its previous action.');
|
|
365
|
+
const completionReview = task.gatewayTarget?.adapter === 'browser' && task.state === 'needs_reconciliation' &&
|
|
366
|
+
Boolean(task.activeAttemptId) && task.browserReport?.status === 'needs_verification' &&
|
|
367
|
+
['COMPLETION_CANDIDATE', 'VERIFICATION_FAILED'].includes(task.browserReport.reason) &&
|
|
368
|
+
!task.browserReport.providerFailure && task.browserReport.lastAction?.outcome !== 'unknown';
|
|
369
|
+
if (task.automationController === 'user') {
|
|
370
|
+
// Jev gave up on this utterance and handed it to the agent: exactly one
|
|
371
|
+
// command, as the user's own next direct command (user keeps control).
|
|
372
|
+
const handoff = this.agentHandoff(context, task, expectedRevision, mode, browserFields, computerInputs, startUrl);
|
|
373
|
+
if (handoff)
|
|
374
|
+
return this.applyDirectControl(context.conversationId, taskId, task, { id: handoffCommandId(context.inputId), action: 'revise', text: instruction }, undefined, context.inputId);
|
|
375
|
+
throw new types_1.OrchestrationError('USER_CONTROLS_AUTOMATION');
|
|
376
|
+
}
|
|
377
|
+
const agentControl = !context.execute &&
|
|
378
|
+
['browser', 'computer'].includes(task.gatewayTarget?.adapter ?? '') && task.capabilities.execute && task.ownerPrincipalId === context.principalId &&
|
|
379
|
+
task.state === 'waiting_input' && !task.activeAttemptId && !task.pendingQuestion && task.executionControl?.phase !== 'paused' &&
|
|
380
|
+
['COMMAND_WAITING_INPUT', 'THINKING_WAITING_INPUT', 'COMPLETION_CANDIDATE', 'VERIFICATION_FAILED'].includes(task.browserReport?.reason ?? task.computerReport?.reason ?? '') &&
|
|
381
|
+
!task.browserReport?.providerFailure && task.browserReport?.lastAction?.outcome !== 'unknown' &&
|
|
382
|
+
// Device disconnect/reconnect changes stateVersion without changing the
|
|
383
|
+
// settled action. Authorize this decision against its immutable round,
|
|
384
|
+
// while the current ownership, controller and idle guards still apply.
|
|
385
|
+
Boolean(this.store.get(`SELECT n.id FROM notifications n JOIN tasks t ON t.id=n.task_id
|
|
386
|
+
WHERE n.task_id=? AND n.decision_id=? AND n.status='assigned' AND ${control_notification_1.CURRENT_CONTROL_ROUND_SQL} LIMIT 1`, taskId, context.decisionId));
|
|
387
|
+
// A provider failure before the first action can be replanned by the
|
|
388
|
+
// assigned parent. Never extend this to post-action or uncertain outcomes.
|
|
389
|
+
const preActionProviderFailure = task.computerReport?.status === 'blocked' &&
|
|
390
|
+
task.computerReport.steps === 0 &&
|
|
391
|
+
/^THINKING_HTTP_(429|502|503|504)$/.test(task.computerReport.reason) &&
|
|
392
|
+
!task.computerReport.trace?.some(event => event.outcome === 'unknown');
|
|
393
|
+
const computerRecovery = !context.execute && task.gatewayTarget?.adapter === 'computer' &&
|
|
394
|
+
task.capabilities.execute && task.ownerPrincipalId === context.principalId &&
|
|
395
|
+
task.state === 'failed' && !task.activeAttemptId && task.executionControl?.phase !== 'paused' &&
|
|
396
|
+
(preActionProviderFailure || ['COMPLETION_CANDIDATE', 'VERIFICATION_FAILED', 'NO_SUPPORTED_ACTION', 'COMPUTER_TEXT_UNGROUNDED'].includes(task.computerReport?.reason ?? '')) &&
|
|
397
|
+
Boolean(this.store.get("SELECT id FROM notifications WHERE task_id=? AND decision_id=? AND status='assigned' AND task_state_version=?", taskId, context.decisionId, task.stateVersion));
|
|
398
|
+
const browserRecovery = agentControl || computerRecovery || !context.execute && task.gatewayTarget?.adapter === 'browser' &&
|
|
399
|
+
task.capabilities.execute && task.ownerPrincipalId === context.principalId &&
|
|
400
|
+
!task.browserReport?.providerFailure && task.browserReport?.lastAction?.outcome !== 'unknown' &&
|
|
401
|
+
(completionReview || (task.state === 'failed' && !task.activeAttemptId &&
|
|
402
|
+
['LOW_OPERATION_CONFIDENCE', 'LOW_TARGET_CONFIDENCE', 'STALE_RETRY_BUDGET', 'TARGET_OBSCURED', 'PAGE_CONTENT_UNAVAILABLE', 'MODEL_BLOCKED', 'OBSERVATION_TRUNCATED', 'NO_PROGRESS', 'NO_SUPPORTED_ACTION', 'TEXT_BUDGET'].includes(task.browserReport?.reason ?? ''))) &&
|
|
403
|
+
Boolean(this.store.get("SELECT id FROM notifications WHERE task_id=? AND decision_id=? AND status='assigned' AND task_state_version=?", taskId, context.decisionId, task.stateVersion));
|
|
404
|
+
if (types_1.TERMINAL_TASK_STATES.has(task.state) && !(task.gatewayTarget && (context.execute || browserRecovery) && ['completed', 'failed'].includes(task.state) && !task.activeAttemptId))
|
|
259
405
|
throw new types_1.OrchestrationError('TASK_TERMINAL');
|
|
260
|
-
if (['cancel_requested', 'recovering', 'needs_reconciliation', 'interrupting'].includes(task.state))
|
|
406
|
+
if (['cancel_requested', 'recovering', 'needs_reconciliation', 'interrupting'].includes(task.state) && !(completionReview && (context.execute || browserRecovery)))
|
|
261
407
|
throw new types_1.OrchestrationError('STATE_CONFLICT');
|
|
262
408
|
if (task.revision !== expectedRevision)
|
|
263
409
|
throw new types_1.OrchestrationError('REVISION_CONFLICT');
|
|
264
|
-
if (task.gatewayTarget && task.
|
|
265
|
-
throw new types_1.OrchestrationError('
|
|
410
|
+
if (task.gatewayTarget && (task.ownerPrincipalId !== context.principalId || (!context.execute && !browserRecovery)))
|
|
411
|
+
throw new types_1.OrchestrationError('EXECUTION_DENIED');
|
|
412
|
+
if (task.gatewayTarget && mode !== 'when_ready')
|
|
413
|
+
throw new types_1.OrchestrationError('INVALID_INPUT', 'Use when_ready for Gateway-managed tasks; the current request must settle before revised instructions run.');
|
|
414
|
+
if (browserFields !== undefined && task.gatewayTarget?.adapter !== 'browser')
|
|
415
|
+
throw new types_1.OrchestrationError('INVALID_BROWSER_FIELDS');
|
|
266
416
|
const priorRevision = this.revision(taskId, expectedRevision);
|
|
267
|
-
if (!
|
|
417
|
+
if (browserRecovery && !agentControl && ((priorRevision.browserRecoveryCount ?? 0) >= 3 || priorRevision.guidance === instruction))
|
|
418
|
+
throw new types_1.OrchestrationError('BROWSER_RECOVERY_EXHAUSTED', 'Inspect the evidence and explain the unresolved blocker; do not repeat the same plan.');
|
|
419
|
+
if (!context.execute && !browserRecovery) {
|
|
268
420
|
// A worker progress report increments stateVersion without revoking this
|
|
269
421
|
// alert. Bind advice to the immutable alert and attempt, not that counter.
|
|
270
422
|
const assigned = this.store.get(`SELECT n.id FROM notifications n JOIN conversation_events e
|
|
@@ -278,14 +430,35 @@ class TaskService {
|
|
|
278
430
|
if (!assigned || alreadyAdvised || !task.supervision || task.state !== 'running' || !task.capabilities.execute || mode !== 'when_ready')
|
|
279
431
|
throw new types_1.OrchestrationError('EXECUTION_DENIED');
|
|
280
432
|
}
|
|
433
|
+
if (completionReview) {
|
|
434
|
+
const attempt = this.store.attempt(task.activeAttemptId);
|
|
435
|
+
attempt.state = 'ended';
|
|
436
|
+
this.store.saveAttempt(attempt);
|
|
437
|
+
this.pool.release(task.taskId, false);
|
|
438
|
+
task.activeAttemptId = undefined;
|
|
439
|
+
task.state = 'failed';
|
|
440
|
+
}
|
|
281
441
|
task.revision++;
|
|
282
|
-
const revision = { taskId, revision: task.revision, instructions: instruction,
|
|
283
|
-
contextRefs: priorRevision.contextRefs, mode, originatingInputId: context.inputId,
|
|
284
|
-
...(!context.execute ? { instructions: priorRevision.instructions, answers: priorRevision.answers, originatingInputId: priorRevision.originatingInputId, guidance: instruction, guidanceBasis: { attemptId: task.activeAttemptId, workflowVersion: task.workflow?.version ?? 0, progressAt: task.latestProgress?.observedAt ?? 0 } } : {})
|
|
442
|
+
const revision = { ...(browserUrl ? { browserNavigation: { url: browserUrl, revision: task.revision } } : {}), requestBrowserConsent: context.execute && task.gatewayTarget?.adapter === 'browser', taskId, revision: task.revision, instructions: instruction, computerInputs: context.execute || computerRecovery || agentControl ? computerInputs : priorRevision.computerInputs,
|
|
443
|
+
answers: prepared, contextRefs: priorRevision.contextRefs, mode, originatingInputId: context.inputId,
|
|
444
|
+
...(!context.execute && !computerRecovery && !agentControl ? { instructions: priorRevision.instructions, answers: prepared ?? priorRevision.answers, originatingInputId: priorRevision.originatingInputId, guidance: instruction, guidanceBasis: { attemptId: task.activeAttemptId, workflowVersion: task.workflow?.version ?? 0, progressAt: task.latestProgress?.observedAt ?? 0 } } : {}),
|
|
445
|
+
...(browserRecovery && !agentControl ? { browserRecoveryCount: (priorRevision.browserRecoveryCount ?? 0) + 1, guidanceBasis: undefined } : {}) };
|
|
285
446
|
this.store.run('INSERT INTO task_revisions VALUES(?,?,?)', taskId, task.revision, JSON.stringify(revision));
|
|
447
|
+
if (task.gatewayTarget && ['completed', 'failed'].includes(task.state)) {
|
|
448
|
+
task.state = 'queued';
|
|
449
|
+
delete task.gatewayDispatch;
|
|
450
|
+
delete task.executionControl;
|
|
451
|
+
delete task.failure;
|
|
452
|
+
delete task.result;
|
|
453
|
+
delete task.browserReport;
|
|
454
|
+
delete task.computerReport;
|
|
455
|
+
delete task.latestProgress;
|
|
456
|
+
if (context.execute)
|
|
457
|
+
task.initiatingInputId = context.inputId;
|
|
458
|
+
}
|
|
286
459
|
if (task.state === 'waiting_input') {
|
|
287
460
|
task.pendingQuestion = undefined;
|
|
288
|
-
task.state = task.activeAttemptId ? 'interrupting' : 'queued';
|
|
461
|
+
task.state = task.activeAttemptId ? (task.gatewayTarget ? 'running' : 'interrupting') : 'queued';
|
|
289
462
|
}
|
|
290
463
|
else if (mode === 'interrupt_and_resume' && task.activeAttemptId)
|
|
291
464
|
task.state = 'interrupting';
|
|
@@ -295,7 +468,152 @@ class TaskService {
|
|
|
295
468
|
return task;
|
|
296
469
|
}, taskId);
|
|
297
470
|
}
|
|
471
|
+
/** Direct authenticated user control: no inference turn, no second task. */
|
|
472
|
+
controlByUser(conversationId, principalId, taskId, command) {
|
|
473
|
+
return this.store.transaction(() => {
|
|
474
|
+
this.store.assertMember(conversationId, principalId);
|
|
475
|
+
const task = this.owned(taskId, conversationId);
|
|
476
|
+
if (task.ownerPrincipalId !== principalId)
|
|
477
|
+
throw new types_1.OrchestrationError('ACCESS_DENIED');
|
|
478
|
+
if (!/^[0-9a-f-]{36}$/i.test(command.id) || !['pause', 'revise', 'resume', 'agent', 'user'].includes(command.action) || !Number.isSafeInteger(command.expectedRevision))
|
|
479
|
+
throw new types_1.OrchestrationError('INVALID_INPUT');
|
|
480
|
+
const id = 'user-control:' + command.id, hash = (0, store_1.payloadHash)({ taskId, ...command });
|
|
481
|
+
const prior = this.store.get('SELECT * FROM task_commands WHERE action_id=?', id);
|
|
482
|
+
if (prior) {
|
|
483
|
+
if (prior.principal_id !== principalId || prior.conversation_id !== conversationId)
|
|
484
|
+
throw new types_1.OrchestrationError('ACCESS_DENIED');
|
|
485
|
+
if (prior.payload_hash !== hash)
|
|
486
|
+
throw new types_1.OrchestrationError('IDEMPOTENCY_CONFLICT');
|
|
487
|
+
return task;
|
|
488
|
+
}
|
|
489
|
+
if (!['browser', 'computer'].includes(task.gatewayTarget?.adapter ?? '') || !task.capabilities.execute)
|
|
490
|
+
throw new types_1.OrchestrationError('EXECUTION_DENIED');
|
|
491
|
+
if ((0, automation_session_1.automationSession)(task)?.status === 'closed')
|
|
492
|
+
throw new types_1.OrchestrationError('AUTOMATION_SESSION_CLOSED');
|
|
493
|
+
if (task.revision !== command.expectedRevision)
|
|
494
|
+
throw new types_1.OrchestrationError('REVISION_CONFLICT');
|
|
495
|
+
if (command.action === 'agent' || command.action === 'user') {
|
|
496
|
+
if (command.text !== undefined)
|
|
497
|
+
throw new types_1.OrchestrationError('INVALID_INPUT');
|
|
498
|
+
task.automationController = command.action;
|
|
499
|
+
// The agent drives now: the user's queued direct commands must not run under it.
|
|
500
|
+
if (command.action === 'agent' && task.queuedCommands?.length) {
|
|
501
|
+
const queue = task.queuedCommands;
|
|
502
|
+
delete task.queuedCommands;
|
|
503
|
+
task.latestProgress = { source: 'runtime', observedAt: Date.now(), text: `Control handed to the agent. ${queue.length} queued command${queue.length === 1 ? ' was' : 's were'} not sent: ${queue.map(c => JSON.stringify(c.text.slice(0, 60))).join(', ')}.`.slice(0, 4096) };
|
|
504
|
+
this.store.appendEvent(conversationId, 'task.command_queue_dropped', { taskId, count: queue.length, reason: 'agent_control' }, taskId);
|
|
505
|
+
}
|
|
506
|
+
this.store.saveTask(task, task.stateVersion);
|
|
507
|
+
this.store.run('INSERT INTO task_commands VALUES(?,?,?,?,?,?,?,?,?)', id, taskId, conversationId, principalId, null, 'user_control', hash, JSON.stringify(task), Date.now());
|
|
508
|
+
if (command.action === 'agent' && task.state === 'waiting_input' && !task.activeAttemptId && !task.pendingQuestion && task.executionControl?.phase !== 'paused')
|
|
509
|
+
this.notify(task);
|
|
510
|
+
return task;
|
|
511
|
+
}
|
|
512
|
+
return this.applyDirectControl(conversationId, taskId, task, { id: command.id, action: command.action, text: command.text }, task => this.store.run('INSERT INTO task_commands VALUES(?,?,?,?,?,?,?,?,?)', id, taskId, conversationId, principalId, null, 'user_control', hash, JSON.stringify(task), Date.now()));
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* The agent turn for a handed-off utterance (needs_agent AGENT_HANDOFF receipt
|
|
517
|
+
* on this input and task) may send one command: same task, the receipt's
|
|
518
|
+
* revision (no newer user command), the round settled and waiting, no fields,
|
|
519
|
+
* inputs or navigation, and never twice for the same utterance.
|
|
520
|
+
*/
|
|
521
|
+
agentHandoff(context, task, expectedRevision, mode, browserFields, computerInputs, startUrl) {
|
|
522
|
+
const receipt = this.liveReceipt(context.inputId);
|
|
523
|
+
if (receipt?.code !== 'AGENT_HANDOFF' || receipt.status !== 'needs_agent' || receipt.taskId !== task.taskId)
|
|
524
|
+
return false;
|
|
525
|
+
if (!context.execute || task.ownerPrincipalId !== context.principalId)
|
|
526
|
+
throw new types_1.OrchestrationError('EXECUTION_DENIED');
|
|
527
|
+
if (this.store.get("SELECT 1 FROM task_revisions WHERE task_id=? AND json_extract(payload_json,'$.agentHandoffInputId')=?", task.taskId, context.inputId))
|
|
528
|
+
throw new types_1.OrchestrationError('AGENT_HANDOFF_USED', 'The one command for this handed-off utterance was already sent. Report its outcome; do not send another.');
|
|
529
|
+
if (mode !== 'when_ready' || browserFields !== undefined || computerInputs !== undefined || startUrl !== undefined)
|
|
530
|
+
throw new types_1.OrchestrationError('INVALID_INPUT', 'A handed-off command is one plain next command: mode=when_ready, no fields, inputs or start_url.');
|
|
531
|
+
if (expectedRevision !== receipt.revision || task.revision !== receipt.revision)
|
|
532
|
+
throw new types_1.OrchestrationError('REVISION_CONFLICT', 'The user has given a newer command since this utterance; do not act on it.');
|
|
533
|
+
if (task.activeAttemptId || !((0, automation_session_1.pausedForCommand)(task) || (0, automation_session_1.stoppedForCommand)(task)))
|
|
534
|
+
throw new types_1.OrchestrationError('STATE_CONFLICT');
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
/** The latest live-control receipt of an input (live-execution-input.ts liveControlReceipt). */
|
|
538
|
+
liveReceipt(inputId) {
|
|
539
|
+
const row = this.store.get("SELECT payload_json FROM conversation_events WHERE type='input.execution_control' AND json_extract(payload_json,'$.payload.inputId')=? ORDER BY seq DESC LIMIT 1", inputId);
|
|
540
|
+
return row ? JSON.parse(String(row.payload_json)).payload : undefined;
|
|
541
|
+
}
|
|
542
|
+
/** The owner's pause/revise/resume, or (agentHandoffInputId) the agent's single command for a handed-off utterance. Runs inside the caller's transaction. */
|
|
543
|
+
applyDirectControl(conversationId, taskId, task, command, record, agentHandoffInputId) {
|
|
544
|
+
task.automationController = 'user';
|
|
545
|
+
const paused = (0, automation_session_1.pausedForCommand)(task);
|
|
546
|
+
const recoverable = command.action === 'revise' && (0, automation_session_1.stoppedForCommand)(task);
|
|
547
|
+
if (!['queued', 'starting', 'running', 'interrupting'].includes(task.state) && !paused && !recoverable)
|
|
548
|
+
throw new types_1.OrchestrationError('STATE_CONFLICT');
|
|
549
|
+
// Only the owner's own pause (settled or still settling) leaves interrupted work. A settled
|
|
550
|
+
// round waiting for the next command has nothing to resume: re-running it would replay it.
|
|
551
|
+
if (command.action === 'resume' && !(task.executionControl?.action === 'pause' && ['pending', 'paused'].includes(task.executionControl.phase)))
|
|
552
|
+
throw new types_1.OrchestrationError('NOTHING_TO_RESUME', 'Nothing to resume: the last command already finished. Give the next command instead.');
|
|
553
|
+
if (command.action === 'revise')
|
|
554
|
+
(0, store_1.boundedText)(command.text ?? '', 4000);
|
|
555
|
+
else if (command.text !== undefined)
|
|
556
|
+
throw new types_1.OrchestrationError('INVALID_INPUT');
|
|
557
|
+
const previous = this.revision(taskId, task.revision);
|
|
558
|
+
// Never drop queued words silently: say which commands will not run.
|
|
559
|
+
const unsent = command.action === 'pause' ? task.queuedCommands ?? [] : [];
|
|
560
|
+
const dropped = unsent.length ? ` ${unsent.length} queued command${unsent.length === 1 ? ' was' : 's were'} not sent: ${unsent.map(c => JSON.stringify(c.text.slice(0, 60))).join(', ')}.` : '';
|
|
561
|
+
if (unsent.length)
|
|
562
|
+
this.store.appendEvent(conversationId, 'task.command_queue_dropped', { taskId, count: unsent.length, reason: 'paused' }, taskId);
|
|
563
|
+
if (command.action === 'pause')
|
|
564
|
+
delete task.queuedCommands;
|
|
565
|
+
// A user's own command is still settling: the next one waits its turn,
|
|
566
|
+
// verbatim, instead of superseding it or being folded into a correction.
|
|
567
|
+
if (command.action === 'revise' && this.queuesDirectCommand(task, previous)) {
|
|
568
|
+
const queue = task.queuedCommands ?? [];
|
|
569
|
+
if (queue.length >= COMMAND_QUEUE_MAX)
|
|
570
|
+
throw new types_1.OrchestrationError('COMMAND_QUEUE_FULL', 'Too many commands are waiting. Wait for the current ones to finish or pause.');
|
|
571
|
+
task.queuedCommands = [...queue, { id: command.id, text: command.text, at: Date.now() }];
|
|
572
|
+
task.latestProgress = { source: 'runtime', observedAt: Date.now(), text: `Queued: ${JSON.stringify(command.text.slice(0, 120))} runs after the current command (${task.queuedCommands.length} waiting).` };
|
|
573
|
+
this.store.saveTask(task, task.stateVersion);
|
|
574
|
+
this.store.appendEvent(conversationId, 'task.command_queued', { taskId, commandId: command.id, position: task.queuedCommands.length }, taskId);
|
|
575
|
+
record?.(task);
|
|
576
|
+
return task;
|
|
577
|
+
}
|
|
578
|
+
const priorAnswers = previous.answers?.map(a => ({ field: a.browserFieldLabel ?? a.computerFieldLabel, text: a.text }));
|
|
579
|
+
const nextCommand = command.action === 'revise' && (paused || recoverable);
|
|
580
|
+
const instructions = nextCommand ? command.text : command.action === 'revise' ? CORRECTION_HEAD + command.text + '\n\nEarlier requirements and corrections, newest first. Keep only requirements compatible with the latest correction; do not perform superseded actions:\n' + previous.instructions + (priorAnswers?.length ? '\n\nEarlier user answers (subject to the latest correction):\n' + JSON.stringify(priorAnswers) : '') : previous.instructions;
|
|
581
|
+
(0, store_1.boundedText)(instructions, task.gatewayTarget?.adapter === 'browser' ? 8000 : 16000);
|
|
582
|
+
task.revision++;
|
|
583
|
+
if (recoverable && !paused) {
|
|
584
|
+
delete task.computerReport;
|
|
585
|
+
delete task.failure;
|
|
586
|
+
delete task.browserReport;
|
|
587
|
+
delete task.gatewayDispatch;
|
|
588
|
+
}
|
|
589
|
+
// The owner's new command supersedes any pending question (saveTask closes its record).
|
|
590
|
+
if (command.action === 'revise')
|
|
591
|
+
delete task.pendingQuestion;
|
|
592
|
+
this.store.run('INSERT INTO task_revisions VALUES(?,?,?)', taskId, task.revision, JSON.stringify({ ...previous, requestBrowserConsent: command.action !== 'pause', revision: task.revision, instructions, mode: 'interrupt_and_resume', computerInputs: command.action === 'revise' ? undefined : previous.computerInputs, answers: command.action === 'revise' ? undefined : previous.answers, browserRecoveryCount: 0, guidance: undefined, guidanceBasis: undefined, ...(command.action === 'revise' ? { directCommand: true } : {}), agentHandoffInputId }));
|
|
593
|
+
task.executionControl = { id: command.id, action: command.action, revision: task.revision, phase: task.activeAttemptId ? 'pending' : command.action === 'pause' ? 'paused' : 'pending', requestedAt: Date.now(), ...(agentHandoffInputId ? { agentHandoff: true } : {}) };
|
|
594
|
+
task.state = task.activeAttemptId ? 'interrupting' : command.action === 'pause' ? 'waiting_input' : 'queued';
|
|
595
|
+
task.latestProgress = { source: 'runtime', observedAt: Date.now(), text: ((task.activeAttemptId ? 'Control accepted; settling the current action before applying it.' : command.action === 'pause' ? 'Automation paused.' : 'Control accepted; resuming from a fresh observation.') + dropped).slice(0, 4096) };
|
|
596
|
+
this.store.saveTask(task, task.stateVersion);
|
|
597
|
+
this.store.appendEvent(conversationId, 'task.control_accepted', { taskId, control: task.executionControl }, taskId);
|
|
598
|
+
record?.(task);
|
|
599
|
+
return task;
|
|
600
|
+
}
|
|
601
|
+
/** A user's direct command is still settling, so a new one joins the FIFO queue. */
|
|
602
|
+
queuesDirectCommand(task, previous) {
|
|
603
|
+
return ['browser', 'computer'].includes(task.gatewayTarget?.adapter ?? '') && previous.directCommand === true && (['queued', 'starting', 'running'].includes(task.state) || (task.state === 'interrupting' && task.executionControl?.action === 'revise'));
|
|
604
|
+
}
|
|
605
|
+
/** Speech while a direct command settles is the next command, not an interruption:
|
|
606
|
+
* pausing first would fold it into a correction of the running command. */
|
|
607
|
+
voicePauseApplies(taskId) {
|
|
608
|
+
const task = this.store.task(taskId);
|
|
609
|
+
if (!task)
|
|
610
|
+
return false;
|
|
611
|
+
return !(task.automationController === 'user' && this.queuesDirectCommand(task, this.revision(taskId, task.revision)));
|
|
612
|
+
}
|
|
298
613
|
cancel(context, taskId, replacedByTaskId) {
|
|
614
|
+
// A handed-off utterance allows one task_update only; it never ends the user's session.
|
|
615
|
+
if (this.liveReceipt(context.inputId)?.code === 'AGENT_HANDOFF')
|
|
616
|
+
throw new types_1.OrchestrationError('AGENT_HANDOFF_ONE_COMMAND', 'This input may only become one task_update mode=when_ready on the handed-off task.');
|
|
299
617
|
return this.command(context, 'cancel', { taskId, ...(replacedByTaskId ? { replacedByTaskId } : {}) }, false, () => this.cancelOwned(context.conversationId, taskId, replacedByTaskId), taskId);
|
|
300
618
|
}
|
|
301
619
|
/** Authenticated user control bypasses inference, but retains conversation ownership and task fencing. */
|
|
@@ -314,23 +632,38 @@ class TaskService {
|
|
|
314
632
|
throw new types_1.OrchestrationError('INVALID_REPLACEMENT');
|
|
315
633
|
this.owned(replacedByTaskId, conversationId);
|
|
316
634
|
}
|
|
317
|
-
if (types_1.TERMINAL_TASK_STATES.has(task.state))
|
|
635
|
+
if (types_1.TERMINAL_TASK_STATES.has(task.state) && !(task.gatewayTarget?.adapter === 'computer' && task.gatewayDispatch && (0, automation_session_1.automationSession)(task)?.status !== 'closed')) {
|
|
636
|
+
const session = (0, automation_session_1.automationSession)(task);
|
|
637
|
+
if (session && session.status !== 'closed') {
|
|
638
|
+
task.automationSession = { ...session, status: 'closed', closedAt: Date.now(), closedReason: requestedBy };
|
|
639
|
+
this.store.saveTask(task, task.stateVersion);
|
|
640
|
+
}
|
|
318
641
|
return task;
|
|
642
|
+
}
|
|
319
643
|
if (task.state === 'cancel_requested') {
|
|
320
|
-
|
|
644
|
+
// A user's explicit stop wins; an agent cancel must not erase the stall evidence.
|
|
645
|
+
const overridesStall = task.cancellation?.requestedBy === 'supervisor' && requestedBy === 'user';
|
|
646
|
+
if (overridesStall)
|
|
647
|
+
task.cancellation = { requestedBy, requestedAt: Date.now() };
|
|
648
|
+
if (replacedByTaskId)
|
|
321
649
|
task.replacedByTaskId = replacedByTaskId;
|
|
650
|
+
if (replacedByTaskId || overridesStall)
|
|
322
651
|
this.store.saveTask(task, task.stateVersion);
|
|
323
|
-
}
|
|
324
652
|
return task;
|
|
325
653
|
}
|
|
326
654
|
if (replacedByTaskId)
|
|
327
655
|
task.replacedByTaskId = replacedByTaskId;
|
|
328
656
|
const version = task.stateVersion;
|
|
329
|
-
|
|
330
|
-
|
|
657
|
+
// Retrying an unconfirmed stall stop ("Retry cleanup") finishes the supervisor's
|
|
658
|
+
// stop; it must not turn that stall into a user/agent cancellation.
|
|
659
|
+
const retriesStall = task.state === 'needs_reconciliation' && task.cancellation?.requestedBy === 'supervisor';
|
|
660
|
+
if (!retriesStall)
|
|
661
|
+
task.cancellation = { requestedBy, requestedAt: Date.now() };
|
|
662
|
+
delete task.queuedCommands;
|
|
663
|
+
task.state = task.activeAttemptId || (task.gatewayTarget?.adapter === 'computer' && task.gatewayDispatch) ? 'cancel_requested' : 'cancelled';
|
|
331
664
|
task.pendingQuestion = undefined;
|
|
332
665
|
delete task.failure;
|
|
333
|
-
task.latestProgress = { source: 'runtime', observedAt: Date.now(), text: task.
|
|
666
|
+
task.latestProgress = { source: 'runtime', observedAt: Date.now(), text: task.state === 'cancel_requested' ? 'Disconnecting; waiting for execution and access to stop.' : 'Session ended. Existing files and prior effects are retained.' };
|
|
334
667
|
this.store.saveTask(task, version);
|
|
335
668
|
if (task.state === 'cancelled')
|
|
336
669
|
this.notify(task);
|
|
@@ -338,24 +671,41 @@ class TaskService {
|
|
|
338
671
|
this.store.enqueue('interrupt', `cancel:${taskId}:${task.stateVersion}`, { taskId });
|
|
339
672
|
return task;
|
|
340
673
|
}
|
|
674
|
+
/** A settled computer round still owns device access until explicit revocation. */
|
|
675
|
+
finishIdleComputerCleanup(taskId, requestId) {
|
|
676
|
+
this.store.transaction(() => {
|
|
677
|
+
const task = this.store.task(taskId);
|
|
678
|
+
if (!task || task.state !== 'cancel_requested' || task.activeAttemptId || task.gatewayTarget?.adapter !== 'computer' || task.gatewayDispatch?.requestId !== requestId)
|
|
679
|
+
throw new types_1.OrchestrationError('STALE_ATTEMPT');
|
|
680
|
+
task.state = 'cancelled';
|
|
681
|
+
task.latestProgress = { source: 'runtime', observedAt: Date.now(), text: 'Computer Use disconnected.' };
|
|
682
|
+
this.store.saveTask(task, task.stateVersion);
|
|
683
|
+
this.notify(task);
|
|
684
|
+
});
|
|
685
|
+
}
|
|
341
686
|
/** Scheduler-only cleanup acknowledgment; never authorizes new worker writes. */
|
|
342
687
|
finishCleanup(attemptId, generation, stopped) {
|
|
343
688
|
this.store.transaction(() => {
|
|
344
689
|
const attempt = this.store.attempt(attemptId), task = attempt && this.store.task(attempt.taskId);
|
|
345
690
|
if (!attempt || !task || attempt.generation !== generation || task.activeAttemptId !== attemptId || task.state !== 'cancel_requested')
|
|
346
691
|
throw new types_1.OrchestrationError('STALE_ATTEMPT');
|
|
347
|
-
const
|
|
348
|
-
|
|
692
|
+
const stalled = this.stalledFailure(task, stopped);
|
|
693
|
+
const message = stalled ? stalled.message : stopped ? `Cancelled by ${task.cancellation?.requestedBy ?? 'agent'}. Task execution stopped; existing files and prior effects are retained.` : 'Cannot verify that this task has stopped. Retry cleanup; no unrelated processes were stopped.';
|
|
694
|
+
task.state = stopped ? (stalled ? 'failed' : 'cancelled') : 'needs_reconciliation';
|
|
349
695
|
task.latestProgress = { source: 'runtime', observedAt: Date.now(), text: message };
|
|
350
696
|
if (stopped) {
|
|
351
697
|
attempt.state = 'ended';
|
|
352
698
|
task.activeAttemptId = undefined;
|
|
353
699
|
delete task.failure;
|
|
700
|
+
if (stalled) {
|
|
701
|
+
attempt.failure = stalled;
|
|
702
|
+
task.failure = stalled;
|
|
703
|
+
}
|
|
354
704
|
this.pool.release(task.taskId, false);
|
|
355
705
|
}
|
|
356
706
|
else {
|
|
357
707
|
attempt.state = 'unknown';
|
|
358
|
-
task.failure = { code: 'CLEANUP_UNCONFIRMED', message, observedAt: Date.now() };
|
|
708
|
+
task.failure = stalled ?? { code: 'CLEANUP_UNCONFIRMED', message, observedAt: Date.now() };
|
|
359
709
|
}
|
|
360
710
|
this.store.saveAttempt(attempt);
|
|
361
711
|
this.store.saveTask(task, task.stateVersion);
|
|
@@ -363,9 +713,35 @@ class TaskService {
|
|
|
363
713
|
this.notify(task);
|
|
364
714
|
});
|
|
365
715
|
}
|
|
366
|
-
answer(context, taskId, questionId, answer) {
|
|
716
|
+
answer(context, taskId, questionId, answer, browserFields, computerInputs) {
|
|
367
717
|
(0, store_1.boundedText)(answer);
|
|
368
|
-
|
|
718
|
+
if (computerInputs !== undefined)
|
|
719
|
+
computerInputs = computer_inputs_1.ComputerInputs.parse(computerInputs);
|
|
720
|
+
const prepared = (0, browser_fields_1.preparedBrowserAnswers)(browserFields, context.inputId);
|
|
721
|
+
return this.command(context, 'answer', { taskId, questionId, answer, browserFields, computerInputs }, context.execute, () => {
|
|
722
|
+
if (!context.execute) {
|
|
723
|
+
const task = this.owned(taskId, context.conversationId);
|
|
724
|
+
this.assertQuestion(task, questionId);
|
|
725
|
+
// A notification can fill missing browser text in its own authorized task,
|
|
726
|
+
// never approve a new operation, answer an unrelated task, or grant consent.
|
|
727
|
+
const assigned = this.store.get(`SELECT n.id FROM notifications n JOIN conversation_events e
|
|
728
|
+
ON e.conversation_id=n.conversation_id AND e.type='task.state_changed'
|
|
729
|
+
AND json_extract(e.payload_json,'$.payload.taskId')=n.task_id
|
|
730
|
+
AND json_extract(e.payload_json,'$.payload.stateVersion')=n.task_state_version
|
|
731
|
+
WHERE n.task_id=? AND n.conversation_id=? AND n.decision_id=? AND n.status='assigned'
|
|
732
|
+
AND json_extract(e.payload_json,'$.payload.pendingQuestion.questionId')=? LIMIT 1`, taskId, context.conversationId, context.decisionId, questionId);
|
|
733
|
+
const reviewed = context.questionReviewIds?.includes(questionId) && this.store.get('SELECT question_id FROM task_questions WHERE question_id=? AND task_id=? AND closed=0 AND revision=?', questionId, taskId, task.revision);
|
|
734
|
+
const missingBrowser = task.gatewayTarget?.adapter === 'browser' && task.browserReport?.reason === 'FIELD_TEXT_REQUIRED' && task.browserReport.fieldRequest?.reason === 'missing' && Boolean(task.browserReport.fieldRequest.label);
|
|
735
|
+
const missingComputer = task.gatewayTarget?.adapter === 'computer' && task.computerReport?.reason === 'FIELD_TEXT_REQUIRED' && task.computerReport.fieldRequest?.reason === 'missing' && Boolean(task.computerReport.fieldRequest.label);
|
|
736
|
+
if ((!assigned && !reviewed) || task.ownerPrincipalId !== context.principalId || !task.capabilities.execute || (!missingBrowser && !missingComputer))
|
|
737
|
+
throw new types_1.OrchestrationError('EXECUTION_DENIED', `EXECUTION_DENIED: This is an internal report/non-executing turn (execute capability is off for this turn), so it cannot commit a task answer here. This is a turn-scoped permission boundary — not a system outage, and not proof that a prior answer was rejected or lost: any task command already committed on an executing turn stays durably saved and reaches the worker when it resumes. Do not retry this mutation on this turn or report the service as unavailable; reconcile the worker's request against the committed answer/receipt, and surface only a genuinely new decision to the user on a normal turn.`);
|
|
738
|
+
}
|
|
739
|
+
if (browserFields !== undefined && this.owned(taskId, context.conversationId).gatewayTarget?.adapter !== 'browser')
|
|
740
|
+
throw new types_1.OrchestrationError('INVALID_BROWSER_FIELDS');
|
|
741
|
+
if (computerInputs !== undefined && this.owned(taskId, context.conversationId).gatewayTarget?.adapter !== 'computer')
|
|
742
|
+
throw new types_1.OrchestrationError('INVALID_INPUT');
|
|
743
|
+
return this.answerOwned(context.conversationId, taskId, questionId, answer, context.inputId, prepared, computerInputs);
|
|
744
|
+
}, taskId);
|
|
369
745
|
}
|
|
370
746
|
/** A scoped authenticated reply is user authorization, without a fabricated model decision. */
|
|
371
747
|
answerByUser(conversationId, principalId, taskId, questionId, answer, acceptedInputId) {
|
|
@@ -426,18 +802,24 @@ class TaskService {
|
|
|
426
802
|
if (task.state !== 'waiting_input' || task.pendingQuestion?.questionId !== questionId || task.pendingQuestion.revision !== task.revision)
|
|
427
803
|
throw new types_1.OrchestrationError('STALE_QUESTION');
|
|
428
804
|
}
|
|
429
|
-
answerOwned(conversationId, taskId, questionId, answer, inputId) {
|
|
805
|
+
answerOwned(conversationId, taskId, questionId, answer, inputId, prepared, computerInputs) {
|
|
430
806
|
const task = this.owned(taskId, conversationId), version = task.stateVersion;
|
|
431
807
|
this.assertQuestion(task, questionId);
|
|
432
808
|
// Persist early answers, while fencing scheduling until the old attempt really ends.
|
|
433
809
|
const previous = this.revision(taskId, task.revision);
|
|
810
|
+
const browserFieldLabel = task.gatewayTarget?.adapter === 'browser' && task.browserReport?.reason === 'FIELD_TEXT_REQUIRED' && task.browserReport.fieldRequest?.reason === 'missing' ? task.browserReport.fieldRequest.label : undefined;
|
|
811
|
+
if (browserFieldLabel && answer.length > 2000)
|
|
812
|
+
throw new types_1.OrchestrationError('BROWSER_FIELD_VALUE_TOO_LONG');
|
|
813
|
+
const computerField = task.gatewayTarget?.adapter === 'computer' && task.computerReport?.fieldRequest?.reason === 'missing' ? task.computerReport.fieldRequest : undefined;
|
|
814
|
+
if (computerField && answer.length > 2000)
|
|
815
|
+
throw new types_1.OrchestrationError('COMPUTER_FIELD_VALUE_TOO_LONG');
|
|
434
816
|
task.revision++;
|
|
435
|
-
this.store.run('INSERT INTO task_revisions VALUES(?,?,?)', taskId, task.revision, JSON.stringify({ ...previous, revision: task.revision,
|
|
436
|
-
answers: [...(previous.answers ?? []), { questionId, text: answer, inputId }], originatingInputId: inputId }));
|
|
817
|
+
this.store.run('INSERT INTO task_revisions VALUES(?,?,?)', taskId, task.revision, JSON.stringify({ ...previous, ...(computerInputs !== undefined ? { computerInputs } : {}), revision: task.revision,
|
|
818
|
+
answers: [...(previous.answers ?? []), ...(prepared ?? []), { questionId, text: answer, inputId, ...(browserFieldLabel ? { browserFieldLabel } : {}), ...(computerField ? { computerFieldLabel: computerField.label, computerApplication: computerField.application, computerWindowTitle: computerField.windowTitle, computerFieldRole: computerField.role } : {}) }], originatingInputId: inputId }));
|
|
437
819
|
task.pendingQuestion = undefined;
|
|
438
|
-
task.state = task.activeAttemptId ? 'interrupting' : 'queued';
|
|
820
|
+
task.state = task.activeAttemptId ? (task.gatewayTarget ? 'running' : 'interrupting') : 'queued';
|
|
439
821
|
this.store.saveTask(task, version);
|
|
440
|
-
this.store.enqueue(task.activeAttemptId ? 'interrupt' : 'schedule', `answer:${questionId}`, { taskId });
|
|
822
|
+
this.store.enqueue(task.activeAttemptId && !task.gatewayTarget ? 'interrupt' : 'schedule', `answer:${questionId}`, { taskId });
|
|
441
823
|
if (this.store.get("SELECT name FROM sqlite_master WHERE type='table' AND name='conversation_intake'")) {
|
|
442
824
|
const input = this.store.get('SELECT principal_id,binding_id,input_seq FROM conversation_inputs WHERE id=? AND conversation_id=?', inputId, conversationId);
|
|
443
825
|
this.store.run(`DELETE FROM conversation_intake WHERE conversation_id=? AND principal_id=? AND binding_id=?
|
|
@@ -522,6 +904,8 @@ class TaskService {
|
|
|
522
904
|
attempt.processIdentity = identity;
|
|
523
905
|
task.state = 'running';
|
|
524
906
|
task.appliedRevision = attempt.revision;
|
|
907
|
+
if (task.executionControl?.revision === attempt.revision)
|
|
908
|
+
task.executionControl.phase = 'applied';
|
|
525
909
|
this.store.saveAttempt(attempt);
|
|
526
910
|
this.store.saveTask(task, task.stateVersion);
|
|
527
911
|
this.store.appendEvent(task.conversationId, 'task.revision_applied', { revision: attempt.revision }, task.taskId);
|
|
@@ -565,16 +949,51 @@ class TaskService {
|
|
|
565
949
|
this.store.run('UPDATE tasks SET snapshot_json=? WHERE id=? AND active_attempt_id=?', JSON.stringify(task), task.taskId, attemptId);
|
|
566
950
|
this.store.appendEvent(task.conversationId, 'task.execution', observation, task.taskId);
|
|
567
951
|
const attempt = this.store.attempt(attemptId);
|
|
568
|
-
|
|
569
|
-
|
|
952
|
+
const startedAt = attempt.startedAt ?? attempt.processIdentity?.startedAt ?? task.createdAt;
|
|
953
|
+
// Reviews alone never end a task (#557): past the limit without a new
|
|
954
|
+
// worker report, stop the attempt through the cancellation path.
|
|
955
|
+
const reportedAt = Math.max(startedAt, task.latestProgress?.source === 'worker' ? task.latestProgress.observedAt : 0);
|
|
956
|
+
const limit = this.config.tasks.progressStaleLimitMs;
|
|
957
|
+
if (limit && task.state === 'running' && observation.observedAt - reportedAt >= limit) {
|
|
958
|
+
this.stopStalled(task, observation.observedAt - reportedAt, observation);
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
if (observation.observedAt - startedAt >= this.config.tasks.progressStaleMs) {
|
|
962
|
+
if (observation.resultSeenAt !== undefined) {
|
|
963
|
+
this.supervise(task, 'result_seen_not_terminal', `The worker returned a final result ${Math.max(0, Math.round((observation.observedAt - observation.resultSeenAt) / 1000))}s ago, but ${observation.pendingBackground ?? 0} native background task(s) have not reported completion, so the task is not terminal yet. The result is accepted automatically once the background grace period passes without activity. Report what is still being awaited; stop background work that is no longer needed.`, observation.observedAt);
|
|
964
|
+
}
|
|
965
|
+
else {
|
|
966
|
+
this.supervise(task, 'stale_progress', 'A scheduled progress update is due. Report completed steps, the current step, and any concrete dependency being awaited. Use substantive evidence; process/tool activity alone does not prove progress. Do not volunteer reassurance that the task is not stuck or is really running.', observation.observedAt);
|
|
967
|
+
}
|
|
570
968
|
}
|
|
571
969
|
});
|
|
572
970
|
}
|
|
971
|
+
stopStalled(task, quietMs, observation) {
|
|
972
|
+
const minutes = Math.round(quietMs / 60000);
|
|
973
|
+
const reason = `No new worker progress report for ${minutes} minute(s), past tasks.progressStaleLimitMs (${Math.round(this.config.tasks.progressStaleLimitMs / 60000)} minute(s)).` +
|
|
974
|
+
(observation.resultSeenAt !== undefined ? ` A final result was received but ${observation.pendingBackground ?? 0} native background task(s) never reported completion.` : '');
|
|
975
|
+
task.cancellation = { requestedBy: 'supervisor', requestedAt: observation.observedAt, reason };
|
|
976
|
+
task.state = 'cancel_requested';
|
|
977
|
+
task.latestProgress = { source: 'runtime', observedAt: observation.observedAt, text: `Stopping: ${reason}` };
|
|
978
|
+
this.store.saveTask(task, task.stateVersion);
|
|
979
|
+
this.store.appendEvent(task.conversationId, 'task.progress_stalled', { taskId: task.taskId, attemptId: observation.attemptId, quietMs, limitMs: this.config.tasks.progressStaleLimitMs, resultSeenAt: observation.resultSeenAt, pendingBackground: observation.pendingBackground }, task.taskId);
|
|
980
|
+
this.store.enqueue('interrupt', `stalled:${task.taskId}:${task.stateVersion}`, { taskId: task.taskId });
|
|
981
|
+
}
|
|
982
|
+
/** A supervisor stop is a failure with its evidence, not a user/agent cancellation.
|
|
983
|
+
* An unconfirmed stop keeps the cleanup code every other unconfirmed stop uses. */
|
|
984
|
+
stalledFailure(task, stopped = true) {
|
|
985
|
+
if (task.cancellation?.requestedBy !== 'supervisor')
|
|
986
|
+
return undefined;
|
|
987
|
+
const reason = task.cancellation.reason ?? 'No new worker progress report within tasks.progressStaleLimitMs.';
|
|
988
|
+
return stopped
|
|
989
|
+
? { code: 'PROGRESS_STALLED', message: `${reason} Stopped the attempt; existing files and prior effects are retained. Inspect them before retrying.`, observedAt: task.cancellation.requestedAt }
|
|
990
|
+
: { code: 'CLEANUP_UNCONFIRMED', message: `${reason} Cannot verify that the attempt stopped. Retry cleanup; no unrelated processes were stopped.`, observedAt: Date.now() };
|
|
991
|
+
}
|
|
573
992
|
supervise(task, reason, message, now = Date.now()) {
|
|
574
993
|
// Keep internal inspection frequent even when user-facing reports are quiet.
|
|
575
994
|
// The agent independently decides whether this review merits a message.
|
|
576
995
|
const since = task.supervision?.observedAt ?? (task.activeAttemptId ? this.store.attempt(task.activeAttemptId)?.startedAt : undefined) ?? task.createdAt;
|
|
577
|
-
if ((task.supervision || reason
|
|
996
|
+
if ((task.supervision || reason !== 'repeated_tools') && now - since < this.config.tasks.progressNotifyCooldownMs)
|
|
578
997
|
return;
|
|
579
998
|
task.supervision = { id: (0, crypto_1.randomUUID)(), reason, observedAt: now, message };
|
|
580
999
|
this.store.saveTask(task, task.stateVersion);
|
|
@@ -690,7 +1109,7 @@ class TaskService {
|
|
|
690
1109
|
return result;
|
|
691
1110
|
});
|
|
692
1111
|
}
|
|
693
|
-
requestInput(attemptId, generation, question, actionId) {
|
|
1112
|
+
requestInput(attemptId, generation, question, actionId, computerReport) {
|
|
694
1113
|
(0, store_1.boundedText)(question, 4096);
|
|
695
1114
|
return this.store.transaction(() => {
|
|
696
1115
|
const { task, attempt } = this.active(attemptId, generation);
|
|
@@ -699,6 +1118,8 @@ class TaskService {
|
|
|
699
1118
|
return receipt.prior;
|
|
700
1119
|
if (task.state !== 'running' || task.revision !== attempt.revision)
|
|
701
1120
|
throw new types_1.OrchestrationError('SUPERSEDED_QUESTION');
|
|
1121
|
+
if (computerReport && task.gatewayTarget?.adapter === 'computer')
|
|
1122
|
+
task.computerReport = computerReport;
|
|
702
1123
|
task.state = 'waiting_input';
|
|
703
1124
|
task.pendingQuestion = { questionId: (0, crypto_1.randomUUID)(), text: question, revision: attempt.revision };
|
|
704
1125
|
this.store.saveTask(task, task.stateVersion);
|
|
@@ -721,9 +1142,16 @@ class TaskService {
|
|
|
721
1142
|
}
|
|
722
1143
|
return this.store.transaction(() => {
|
|
723
1144
|
const { task, attempt } = this.active(attemptId, generation);
|
|
1145
|
+
if (task.gatewayTarget?.adapter === 'computer' && outcome.computerReport)
|
|
1146
|
+
task.computerReport = outcome.computerReport;
|
|
1147
|
+
if (task.gatewayTarget?.adapter === 'browser' && outcome.browserReport)
|
|
1148
|
+
task.browserReport = outcome.browserReport;
|
|
1149
|
+
if (outcome.computerReport || outcome.browserReport)
|
|
1150
|
+
recordAction(task, attempt.revision, this.revision(task.taskId, attempt.revision).instructions, outcome);
|
|
724
1151
|
// A structured unresolved blocker is not successful task completion.
|
|
725
|
-
|
|
726
|
-
|
|
1152
|
+
const waitingForCommand = outcome.type === 'paused' && ['browser', 'computer'].includes(task.gatewayTarget?.adapter ?? '') && ['THINKING_WAITING_INPUT', 'COMMAND_WAITING_INPUT', 'COMPLETION_CANDIDATE', 'VERIFICATION_FAILED'].includes(outcome.computerReport?.reason ?? outcome.browserReport?.reason ?? '');
|
|
1153
|
+
if (outcome.type === 'paused' && !waitingForCommand && !(task.state === 'waiting_input' && task.pendingQuestion) &&
|
|
1154
|
+
task.state !== 'interrupting' && task.state !== 'cancel_requested' && !(task.gatewayTarget && task.revision > attempt.revision))
|
|
727
1155
|
throw new types_1.OrchestrationError('STATE_CONFLICT');
|
|
728
1156
|
// Keep the full final report, and preserve cancellation / new revisions / questions.
|
|
729
1157
|
const blocked = outcome.type === 'completed' && task.workflow?.attemptId === attemptId &&
|
|
@@ -733,14 +1161,17 @@ class TaskService {
|
|
|
733
1161
|
task.result = outcome.result;
|
|
734
1162
|
outcome = { type: 'failed', failure: { code: 'WORKER_BLOCKED', message: task.workflow.checkpoint.findings.filter(f => f.status === 'open').map(f => f.summary).join('\n').slice(0, 4096), observedAt: Date.now() } };
|
|
735
1163
|
}
|
|
1164
|
+
const stalled = task.state === 'cancel_requested' ? this.stalledFailure(task, outcome.type !== 'unknown') : undefined;
|
|
736
1165
|
if (outcome.type !== 'completed' && outcome.type !== 'paused') {
|
|
737
|
-
attempt.failure = outcome.failure ?? (0, failure_1.taskFailure)(undefined, outcome.type === 'stopped' ? 'WORKER_STOPPED' : 'WORKER_FAILED');
|
|
1166
|
+
attempt.failure = stalled ?? outcome.failure ?? (0, failure_1.taskFailure)(undefined, outcome.type === 'stopped' ? 'WORKER_STOPPED' : 'WORKER_FAILED');
|
|
738
1167
|
task.failure = attempt.failure;
|
|
739
1168
|
}
|
|
740
1169
|
else {
|
|
741
1170
|
delete task.failure;
|
|
742
1171
|
}
|
|
743
1172
|
if (outcome.type === 'unknown') {
|
|
1173
|
+
if (task.executionControl?.phase === 'pending')
|
|
1174
|
+
task.executionControl.phase = 'blocked';
|
|
744
1175
|
attempt.state = 'unknown';
|
|
745
1176
|
task.state = 'needs_reconciliation';
|
|
746
1177
|
}
|
|
@@ -750,51 +1181,222 @@ class TaskService {
|
|
|
750
1181
|
if (outcome.type === 'completed')
|
|
751
1182
|
attempt.result = outcome.result;
|
|
752
1183
|
if (task.state === 'cancel_requested') {
|
|
753
|
-
task.state = outcome.type === 'completed' && task.revision === attempt.revision ? 'completed' : 'cancelled';
|
|
1184
|
+
task.state = outcome.type === 'completed' && task.revision === attempt.revision ? 'completed' : stalled ? 'failed' : 'cancelled';
|
|
754
1185
|
}
|
|
755
1186
|
else if (task.state === 'waiting_input' && task.pendingQuestion) { /* retain question; execution slot now free */ }
|
|
1187
|
+
else if (task.executionControl?.phase === 'pending' && task.executionControl.action === 'pause') {
|
|
1188
|
+
task.state = 'waiting_input';
|
|
1189
|
+
task.executionControl.phase = 'paused';
|
|
1190
|
+
delete task.failure;
|
|
1191
|
+
}
|
|
756
1192
|
else if (task.state === 'interrupting' || task.revision > attempt.revision)
|
|
757
1193
|
task.state = 'queued';
|
|
1194
|
+
else if (waitingForCommand) {
|
|
1195
|
+
task.state = 'waiting_input';
|
|
1196
|
+
task.latestProgress = { source: 'runtime', observedAt: Date.now(), text: task.gatewayTarget?.adapter === 'computer' && outcome.computerReport ? (0, computer_outcome_1.computerOutcomeText)(outcome.computerReport) : task.gatewayTarget?.adapter === 'browser' && outcome.browserReport ? (0, browser_outcome_1.browserOutcomeText)(outcome.browserReport) : 'Waiting for your next command.' };
|
|
1197
|
+
}
|
|
758
1198
|
else
|
|
759
1199
|
task.state = outcome.type === 'completed' ? 'completed' : 'failed';
|
|
760
1200
|
if (task.state === 'completed' && outcome.type === 'completed')
|
|
761
1201
|
task.result = outcome.result;
|
|
762
1202
|
}
|
|
1203
|
+
// A paused or superseded-revision result skips the failure branch above;
|
|
1204
|
+
// a stalled stop still ends failed, so it must carry the stall reason.
|
|
1205
|
+
if (task.state === 'failed' && stalled) {
|
|
1206
|
+
attempt.failure = stalled;
|
|
1207
|
+
task.failure = stalled;
|
|
1208
|
+
task.latestProgress = { source: 'runtime', observedAt: Date.now(), text: stalled.message };
|
|
1209
|
+
}
|
|
1210
|
+
if (task.queuedCommands?.length)
|
|
1211
|
+
this.nextQueuedCommand(task, waitingForCommand || outcome.type === 'completed');
|
|
763
1212
|
if (task.state === 'cancelled') {
|
|
764
1213
|
delete task.failure;
|
|
765
1214
|
task.latestProgress = { source: 'runtime', observedAt: Date.now(), text: `Cancelled by ${task.cancellation?.requestedBy ?? 'agent'}. Existing files and prior effects are retained.` };
|
|
766
1215
|
}
|
|
1216
|
+
if (outcome.type === 'completed' && outcome.result.unresolvedBackground) {
|
|
1217
|
+
this.store.appendEvent(task.conversationId, 'task.background_unresolved', { attemptId, ...outcome.result.unresolvedBackground }, task.taskId);
|
|
1218
|
+
}
|
|
767
1219
|
if (outcome.type !== 'unknown')
|
|
768
1220
|
this.pool.release(task.taskId, outcome.type === 'completed' || outcome.type === 'paused');
|
|
769
1221
|
this.store.saveAttempt(attempt);
|
|
770
1222
|
this.store.saveTask(task, task.stateVersion);
|
|
771
1223
|
if (task.state === 'queued')
|
|
772
1224
|
this.store.enqueue('schedule', `schedule:${task.taskId}:${task.stateVersion}`, { taskId: task.taskId });
|
|
773
|
-
else if (types_1.TERMINAL_TASK_STATES.has(task.state) || task.state === 'needs_reconciliation')
|
|
1225
|
+
else if (types_1.TERMINAL_TASK_STATES.has(task.state) || task.state === 'needs_reconciliation' || (waitingForCommand && task.state === 'waiting_input' && task.automationController !== 'user' && task.executionControl?.phase !== 'paused'))
|
|
774
1226
|
this.notify(task);
|
|
775
1227
|
return task;
|
|
776
1228
|
});
|
|
777
1229
|
}
|
|
1230
|
+
/** Delivers the oldest queued direct command after a settled round, or drops
|
|
1231
|
+
* the queue when the round did not settle cleanly; queued text is never replayed later. */
|
|
1232
|
+
nextQueuedCommand(task, settled) {
|
|
1233
|
+
const queue = task.queuedCommands ?? [];
|
|
1234
|
+
if (!settled || !['waiting_input', 'completed'].includes(task.state) || task.pendingQuestion || task.executionControl?.phase === 'paused' || (0, automation_session_1.automationSession)(task)?.status === 'closed') {
|
|
1235
|
+
if (types_1.TERMINAL_TASK_STATES.has(task.state) || task.state === 'needs_reconciliation' || task.state === 'waiting_input') {
|
|
1236
|
+
const dropped = `${queue.length} queued command${queue.length === 1 ? ' was' : 's were'} not sent because the previous command did not finish normally: ${queue.map(c => JSON.stringify(c.text.slice(0, 60))).join(', ')}.`;
|
|
1237
|
+
task.latestProgress = { source: 'runtime', observedAt: Date.now(), text: [task.latestProgress?.text, dropped].filter(Boolean).join('\n').slice(0, 4096) };
|
|
1238
|
+
delete task.queuedCommands;
|
|
1239
|
+
}
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
const [next, ...rest] = queue;
|
|
1243
|
+
const previous = this.revision(task.taskId, task.revision);
|
|
1244
|
+
task.revision++;
|
|
1245
|
+
this.store.run('INSERT INTO task_revisions VALUES(?,?,?)', task.taskId, task.revision, JSON.stringify({ ...previous, requestBrowserConsent: true, revision: task.revision, instructions: next.text, mode: 'interrupt_and_resume', computerInputs: undefined, answers: undefined, browserRecoveryCount: 0, guidance: undefined, guidanceBasis: undefined, directCommand: true }));
|
|
1246
|
+
task.queuedCommands = rest.length ? rest : undefined;
|
|
1247
|
+
if (!task.queuedCommands)
|
|
1248
|
+
delete task.queuedCommands;
|
|
1249
|
+
if (task.state === 'completed') {
|
|
1250
|
+
delete task.result;
|
|
1251
|
+
delete task.computerReport;
|
|
1252
|
+
delete task.browserReport;
|
|
1253
|
+
delete task.gatewayDispatch;
|
|
1254
|
+
}
|
|
1255
|
+
task.executionControl = { id: next.id, action: 'revise', revision: task.revision, phase: 'pending', requestedAt: Date.now() };
|
|
1256
|
+
task.state = 'queued';
|
|
1257
|
+
const done = task.latestProgress?.text;
|
|
1258
|
+
task.latestProgress = { source: 'runtime', observedAt: Date.now(), text: [done, `Running queued command: ${JSON.stringify(next.text.slice(0, 120))}${rest.length ? ` (${rest.length} more waiting)` : ''}.`].filter(Boolean).join('\n').slice(0, 4096) };
|
|
1259
|
+
}
|
|
778
1260
|
notify(task) {
|
|
779
1261
|
const input = this.store.get('SELECT binding_id FROM conversation_inputs WHERE id=?', task.initiatingInputId);
|
|
780
1262
|
const notificationId = (0, crypto_1.randomUUID)();
|
|
781
1263
|
this.store.run(`INSERT INTO notifications(id,conversation_id,task_id,task_state_version,originating_binding_id) VALUES(?,?,?,?,?) ON CONFLICT(task_id,task_state_version) DO NOTHING`, notificationId, task.conversationId, task.taskId, task.stateVersion, input.binding_id);
|
|
782
1264
|
this.store.enqueue('notification', `notification:${task.taskId}:${task.stateVersion}`, { conversationId: task.conversationId, taskId: task.taskId, notificationId });
|
|
783
1265
|
}
|
|
1266
|
+
/** Parent independently checks a fresh, scoped browser observation. Never clears an uncertain mutation. */
|
|
1267
|
+
verifyBrowser(context, taskId, revision, requestId, evidenceId, evidence, check) {
|
|
1268
|
+
(0, store_1.boundedText)(evidence, 4096);
|
|
1269
|
+
(0, store_1.boundedText)(requestId, 256);
|
|
1270
|
+
(0, store_1.boundedText)(evidenceId, 128);
|
|
1271
|
+
return this.command(context, 'verify_browser', { taskId, revision, requestId, evidenceId, evidence }, context.execute, () => {
|
|
1272
|
+
const task = this.owned(taskId, context.conversationId);
|
|
1273
|
+
if (task.ownerPrincipalId !== context.principalId || task.gatewayTarget?.adapter !== 'browser' || !['needs_reconciliation', 'failed'].includes(task.state) || task.revision !== revision || task.gatewayDispatch?.requestId !== requestId || !task.browserReport || !(0, browser_contract_1.parentVerifiableBrowserResult)(task.browserReport))
|
|
1274
|
+
throw new types_1.OrchestrationError('BROWSER_VERIFICATION_UNAVAILABLE');
|
|
1275
|
+
if (!task.capabilities.execute || (!context.execute && !this.store.get("SELECT id FROM notifications WHERE task_id=? AND decision_id=? AND status='assigned' AND task_state_version=?", taskId, context.decisionId, task.stateVersion)))
|
|
1276
|
+
throw new types_1.OrchestrationError('EXECUTION_DENIED');
|
|
1277
|
+
check();
|
|
1278
|
+
const attemptId = task.activeAttemptId ?? this.store.get("SELECT id FROM task_attempts WHERE task_id=? AND revision=? AND state='ended' ORDER BY generation DESC LIMIT 1", taskId, revision)?.id;
|
|
1279
|
+
const attempt = attemptId ? this.store.attempt(String(attemptId)) : undefined;
|
|
1280
|
+
if (!attempt)
|
|
1281
|
+
throw new types_1.OrchestrationError('BROWSER_VERIFICATION_UNAVAILABLE');
|
|
1282
|
+
attempt.state = 'ended';
|
|
1283
|
+
task.activeAttemptId = undefined;
|
|
1284
|
+
task.state = task.revision > attempt.revision ? 'queued' : 'completed';
|
|
1285
|
+
delete task.failure;
|
|
1286
|
+
delete attempt.failure;
|
|
1287
|
+
task.browserReport.status = 'succeeded';
|
|
1288
|
+
task.browserReport.reason = 'PARENT_VERIFIED';
|
|
1289
|
+
task.browserReport.verification = { source: 'parent', evidence, at: Date.now() };
|
|
1290
|
+
task.result = { summary: 'Parent verified browser result: ' + evidence, artifactIds: [] };
|
|
1291
|
+
attempt.result = task.result;
|
|
1292
|
+
this.pool.release(task.taskId, true);
|
|
1293
|
+
this.store.saveAttempt(attempt);
|
|
1294
|
+
this.store.saveTask(task, task.stateVersion);
|
|
1295
|
+
this.store.appendEvent(task.conversationId, 'browser.parent_verified', { requestId, evidenceId, evidence }, taskId);
|
|
1296
|
+
if (task.state === 'queued')
|
|
1297
|
+
this.store.enqueue('schedule', `schedule:${task.taskId}:${task.stateVersion}`, { taskId: task.taskId });
|
|
1298
|
+
else
|
|
1299
|
+
this.notify(task);
|
|
1300
|
+
return task;
|
|
1301
|
+
}, taskId);
|
|
1302
|
+
}
|
|
1303
|
+
verifyComputer(context, taskId, revision, requestId, evidenceId, evidence, check) {
|
|
1304
|
+
(0, store_1.boundedText)(evidence, 4096);
|
|
1305
|
+
(0, store_1.boundedText)(requestId, 256);
|
|
1306
|
+
(0, store_1.boundedText)(evidenceId, 128);
|
|
1307
|
+
return this.command(context, 'verify_computer', { taskId, revision, requestId, evidenceId, evidence }, context.execute, () => {
|
|
1308
|
+
const task = this.owned(taskId, context.conversationId);
|
|
1309
|
+
if (task.ownerPrincipalId !== context.principalId || task.gatewayTarget?.adapter !== 'computer' || !['failed', 'waiting_input'].includes(task.state) || task.revision !== revision || task.gatewayDispatch?.requestId !== requestId || !task.computerReport || task.computerReport.status !== 'needs_verification')
|
|
1310
|
+
throw new types_1.OrchestrationError('COMPUTER_VERIFICATION_UNAVAILABLE');
|
|
1311
|
+
if (!task.capabilities.execute || (!context.execute && !this.store.get("SELECT id FROM notifications WHERE task_id=? AND decision_id=? AND status='assigned' AND task_state_version=?", taskId, context.decisionId, task.stateVersion)))
|
|
1312
|
+
throw new types_1.OrchestrationError('EXECUTION_DENIED');
|
|
1313
|
+
check();
|
|
1314
|
+
const attemptId = task.activeAttemptId ?? this.store.get("SELECT id FROM task_attempts WHERE task_id=? AND revision=? AND state='ended' ORDER BY generation DESC LIMIT 1", taskId, revision)?.id;
|
|
1315
|
+
const attempt = attemptId ? this.store.attempt(String(attemptId)) : undefined;
|
|
1316
|
+
if (!attempt)
|
|
1317
|
+
throw new types_1.OrchestrationError('COMPUTER_VERIFICATION_UNAVAILABLE');
|
|
1318
|
+
attempt.state = 'ended';
|
|
1319
|
+
task.activeAttemptId = undefined;
|
|
1320
|
+
task.state = task.revision > attempt.revision ? 'queued' : 'completed';
|
|
1321
|
+
delete task.failure;
|
|
1322
|
+
delete attempt.failure;
|
|
1323
|
+
task.computerReport.status = 'succeeded';
|
|
1324
|
+
task.computerReport.reason = 'PARENT_VERIFIED';
|
|
1325
|
+
task.result = { summary: 'Parent verified computer result: ' + evidence, artifactIds: [] };
|
|
1326
|
+
attempt.result = task.result;
|
|
1327
|
+
this.pool.release(task.taskId, true);
|
|
1328
|
+
this.store.saveAttempt(attempt);
|
|
1329
|
+
this.store.saveTask(task, task.stateVersion);
|
|
1330
|
+
this.store.appendEvent(task.conversationId, 'computer.parent_verified', { requestId, evidenceId, evidence }, taskId);
|
|
1331
|
+
if (task.state === 'queued')
|
|
1332
|
+
this.store.enqueue('schedule', `schedule:${task.taskId}:${task.stateVersion}`, { taskId: task.taskId });
|
|
1333
|
+
else
|
|
1334
|
+
this.notify(task);
|
|
1335
|
+
return task;
|
|
1336
|
+
}, taskId);
|
|
1337
|
+
}
|
|
1338
|
+
/** User-requested continuation from fresh evidence, never replay the old dispatch. */
|
|
1339
|
+
reconcileBrowser(context, taskId, revision, requestId, evidenceId, instructions, check) {
|
|
1340
|
+
(0, store_1.boundedText)(instructions, 8000);
|
|
1341
|
+
(0, store_1.boundedText)(requestId, 256);
|
|
1342
|
+
(0, store_1.boundedText)(evidenceId, 128);
|
|
1343
|
+
return this.command(context, 'reconcile_browser', { taskId, revision, requestId, evidenceId, instructions }, true, () => {
|
|
1344
|
+
const task = this.owned(taskId, context.conversationId);
|
|
1345
|
+
if ((0, automation_session_1.automationSession)(task)?.status === 'closed')
|
|
1346
|
+
throw new types_1.OrchestrationError('AUTOMATION_SESSION_CLOSED');
|
|
1347
|
+
if (!context.execute || !task.capabilities.execute || task.ownerPrincipalId !== context.principalId)
|
|
1348
|
+
throw new types_1.OrchestrationError('EXECUTION_DENIED');
|
|
1349
|
+
if (task.gatewayTarget?.adapter !== 'browser' || task.state !== 'needs_reconciliation' || !task.activeAttemptId || task.revision !== revision || task.gatewayDispatch?.requestId !== requestId)
|
|
1350
|
+
throw new types_1.OrchestrationError('STATE_CONFLICT');
|
|
1351
|
+
const resolution = check(), attempt = this.store.attempt(task.activeAttemptId);
|
|
1352
|
+
attempt.state = 'ended';
|
|
1353
|
+
this.store.saveAttempt(attempt);
|
|
1354
|
+
this.pool.release(taskId, false);
|
|
1355
|
+
const previous = this.revision(taskId, revision);
|
|
1356
|
+
task.revision++;
|
|
1357
|
+
task.activeAttemptId = undefined;
|
|
1358
|
+
task.state = 'queued';
|
|
1359
|
+
delete task.failure;
|
|
1360
|
+
delete task.browserReport;
|
|
1361
|
+
delete task.gatewayDispatch;
|
|
1362
|
+
delete task.pendingQuestion;
|
|
1363
|
+
task.executionControl = undefined;
|
|
1364
|
+
this.store.run('INSERT INTO task_revisions VALUES(?,?,?)', taskId, task.revision, JSON.stringify({ ...previous, requestBrowserConsent: true, revision: task.revision, instructions, mode: 'when_ready', originatingInputId: context.inputId, answers: undefined, guidance: undefined, guidanceBasis: undefined, browserRecoveryCount: 0 }));
|
|
1365
|
+
this.store.saveTask(task, task.stateVersion);
|
|
1366
|
+
this.store.appendEvent(task.conversationId, 'browser.continuation_authorized', { requestId, evidenceId, resolution, inputId: context.inputId }, taskId);
|
|
1367
|
+
this.store.enqueue('schedule', `schedule:${task.taskId}:${task.stateVersion}`, { taskId });
|
|
1368
|
+
return task;
|
|
1369
|
+
}, taskId);
|
|
1370
|
+
}
|
|
784
1371
|
/** Offline operator workflow only; not exposed through conversation tools.
|
|
785
1372
|
* Caller holds the instance lock and has checked liveness/side effects. */
|
|
786
1373
|
reconcile(taskId, state, evidence) {
|
|
787
1374
|
(0, store_1.boundedText)(evidence, 4096);
|
|
788
|
-
if (!['queued', 'failed', 'cancelled'].includes(state))
|
|
1375
|
+
if (!['queued', 'failed', 'cancelled', 'waiting_input'].includes(state))
|
|
789
1376
|
throw new types_1.OrchestrationError('INVALID_RECONCILIATION');
|
|
790
1377
|
return this.store.transaction(() => {
|
|
791
1378
|
const task = this.store.task(taskId);
|
|
792
1379
|
if (!task || task.state !== 'needs_reconciliation' || !task.activeAttemptId)
|
|
793
1380
|
throw new types_1.OrchestrationError('RECONCILIATION_NOT_REQUIRED');
|
|
1381
|
+
if (['queued', 'waiting_input'].includes(state) && (0, automation_session_1.automationSession)(task)?.status === 'closed')
|
|
1382
|
+
throw new types_1.OrchestrationError('AUTOMATION_SESSION_CLOSED');
|
|
794
1383
|
const attempt = this.store.attempt(task.activeAttemptId);
|
|
795
1384
|
attempt.state = 'ended';
|
|
796
1385
|
task.activeAttemptId = undefined;
|
|
797
1386
|
task.state = state;
|
|
1387
|
+
if (state === 'cancelled')
|
|
1388
|
+
task.failure = undefined;
|
|
1389
|
+
if (state === 'queued' && task.gatewayTarget) {
|
|
1390
|
+
task.gatewayDispatch = undefined;
|
|
1391
|
+
task.failure = undefined;
|
|
1392
|
+
task.computerReport = undefined;
|
|
1393
|
+
}
|
|
1394
|
+
if (state === 'waiting_input') {
|
|
1395
|
+
task.failure = undefined;
|
|
1396
|
+
task.computerReport = { steps: 0, ...task.computerReport, status: 'needs_input', reason: 'COMMAND_WAITING_INPUT' };
|
|
1397
|
+
if (task.automationSession)
|
|
1398
|
+
task.automationSession = { ...task.automationSession, status: 'idle', idleSince: Date.now() };
|
|
1399
|
+
}
|
|
798
1400
|
this.pool.release(taskId, false);
|
|
799
1401
|
task.pendingQuestion = undefined;
|
|
800
1402
|
task.latestProgress = { source: 'runtime', observedAt: Date.now(), text: `Operator reconciliation: ${evidence}` };
|