vellum 0.2.0 → 0.2.2
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 -2
- package/bun.lock +5 -2
- package/package.json +4 -2
- package/scripts/capture-x-graphql.ts +562 -0
- package/scripts/ipc/check-swift-decoder-drift.ts +2 -1
- package/scripts/test.sh +5 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +161 -34
- package/src/__tests__/account-registry.test.ts +2 -1
- package/src/__tests__/agent-heartbeat-service.test.ts +250 -0
- package/src/__tests__/app-bundler.test.ts +12 -33
- package/src/__tests__/asset-materialize-tool.test.ts +16 -15
- package/src/__tests__/asset-search-tool.test.ts +23 -22
- package/src/__tests__/attachments-store.test.ts +56 -127
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +5 -4
- package/src/__tests__/browser-skill-endstate.test.ts +5 -8
- package/src/__tests__/call-bridge.test.ts +385 -0
- package/src/__tests__/call-constants.test.ts +40 -0
- package/src/__tests__/call-orchestrator.test.ts +454 -0
- package/src/__tests__/call-recovery.test.ts +518 -0
- package/src/__tests__/call-routes-http.test.ts +459 -0
- package/src/__tests__/call-state-machine.test.ts +143 -0
- package/src/__tests__/call-state.test.ts +133 -0
- package/src/__tests__/call-store.test.ts +691 -0
- package/src/__tests__/cli-discover.test.ts +1 -1
- package/src/__tests__/commit-message-enrichment-service.test.ts +550 -0
- package/src/__tests__/compaction.benchmark.test.ts +176 -0
- package/src/__tests__/computer-use-tools.test.ts +250 -0
- package/src/__tests__/config-schema.test.ts +348 -3
- package/src/__tests__/conflict-store.test.ts +2 -1
- package/src/__tests__/contacts-tools.test.ts +331 -0
- package/src/__tests__/conversation-store.test.ts +30 -32
- package/src/__tests__/credential-security-invariants.test.ts +4 -0
- package/src/__tests__/date-context.test.ts +373 -0
- package/src/__tests__/db-schedule-syntax-migration.test.ts +129 -0
- package/src/__tests__/doordash-session.test.ts +9 -0
- package/src/__tests__/fixtures/media-reuse-fixtures.ts +3 -3
- package/src/__tests__/followup-tools.test.ts +303 -0
- package/src/__tests__/handlers-twitter-config.test.ts +718 -0
- package/src/__tests__/intent-routing.test.ts +64 -57
- package/src/__tests__/ipc-roundtrip.benchmark.test.ts +237 -0
- package/src/__tests__/ipc-snapshot.test.ts +96 -28
- package/src/__tests__/llm-usage-store.test.ts +3 -8
- package/src/__tests__/media-generate-image.test.ts +1 -1
- package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
- package/src/__tests__/memory-retrieval.benchmark.test.ts +430 -0
- package/src/__tests__/parallel-tool.benchmark.test.ts +294 -0
- package/src/__tests__/playbook-tools.test.ts +342 -0
- package/src/__tests__/profile-compiler.test.ts +2 -1
- package/src/__tests__/provider-streaming.benchmark.test.ts +773 -0
- package/src/__tests__/recurrence-engine-rruleset.test.ts +78 -0
- package/src/__tests__/recurrence-engine.test.ts +69 -0
- package/src/__tests__/recurrence-types.test.ts +71 -0
- package/src/__tests__/registry.test.ts +17 -10
- package/src/__tests__/relay-server.test.ts +633 -0
- package/src/__tests__/reminder-store.test.ts +6 -3
- package/src/__tests__/reminder.test.ts +43 -77
- package/src/__tests__/run-orchestrator-assistant-events.test.ts +222 -0
- package/src/__tests__/run-orchestrator.test.ts +7 -7
- package/src/__tests__/runtime-attachment-metadata.test.ts +19 -20
- package/src/__tests__/runtime-runs-http.test.ts +5 -23
- package/src/__tests__/runtime-runs.test.ts +11 -11
- package/src/__tests__/schedule-store.test.ts +482 -0
- package/src/__tests__/schedule-tools.test.ts +700 -0
- package/src/__tests__/scheduler-recurrence.test.ts +329 -0
- package/src/__tests__/server-history-render.test.ts +14 -13
- package/src/__tests__/session-error.test.ts +28 -0
- package/src/__tests__/session-init.benchmark.test.ts +462 -0
- package/src/__tests__/session-queue.test.ts +89 -16
- package/src/__tests__/session-runtime-assembly.test.ts +161 -0
- package/src/__tests__/session-surfaces-task-progress.test.ts +104 -0
- package/src/__tests__/signup-e2e.test.ts +2 -1
- package/src/__tests__/skill-projection.benchmark.test.ts +328 -0
- package/src/__tests__/skill-script-runner.test.ts +159 -0
- package/src/__tests__/speaker-identification.test.ts +52 -0
- package/src/__tests__/subagent-manager-notify.test.ts +42 -10
- package/src/__tests__/subagent-tools.test.ts +141 -41
- package/src/__tests__/task-compiler.test.ts +2 -1
- package/src/__tests__/task-runner.test.ts +2 -1
- package/src/__tests__/task-scheduler.test.ts +2 -1
- package/src/__tests__/task-tools.test.ts +49 -56
- package/src/__tests__/tool-audit-listener.test.ts +1 -0
- package/src/__tests__/tool-domain-event-publisher.test.ts +2 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +500 -0
- package/src/__tests__/tool-executor.test.ts +13 -17
- package/src/__tests__/turn-commit.test.ts +273 -2
- package/src/__tests__/twilio-provider.test.ts +143 -0
- package/src/__tests__/twilio-routes.test.ts +789 -0
- package/src/__tests__/twitter-auth-handler.test.ts +581 -0
- package/src/__tests__/view-image-tool.test.ts +217 -0
- package/src/__tests__/workspace-git-service.test.ts +403 -0
- package/src/__tests__/workspace-heartbeat-service.test.ts +141 -2
- package/src/agent-heartbeat/agent-heartbeat-service.ts +155 -0
- package/src/bundler/app-bundler.ts +35 -14
- package/src/calls/call-bridge.ts +95 -0
- package/src/calls/call-constants.ts +48 -0
- package/src/calls/call-domain.ts +276 -0
- package/src/calls/call-orchestrator.ts +390 -0
- package/src/calls/call-recovery.ts +207 -0
- package/src/calls/call-state-machine.ts +68 -0
- package/src/calls/call-state.ts +64 -0
- package/src/calls/call-store.ts +416 -0
- package/src/calls/relay-server.ts +335 -0
- package/src/calls/speaker-identification.ts +213 -0
- package/src/calls/twilio-config.ts +34 -0
- package/src/calls/twilio-provider.ts +173 -0
- package/src/calls/twilio-routes.ts +250 -0
- package/src/calls/types.ts +37 -0
- package/src/calls/voice-provider.ts +14 -0
- package/src/cli/config-commands.ts +334 -0
- package/src/cli/core-commands.ts +776 -0
- package/src/cli/doordash.ts +256 -25
- package/src/cli/ipc-client.ts +82 -0
- package/src/cli/map.ts +246 -0
- package/src/cli/twitter.ts +575 -0
- package/src/cli.ts +7 -5
- package/src/commands/__tests__/cc-command-registry.test.ts +319 -0
- package/src/commands/cc-command-registry.ts +209 -0
- package/src/config/bundled-skills/contacts/SKILL.md +39 -0
- package/src/config/bundled-skills/contacts/TOOLS.json +122 -0
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +9 -0
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +9 -0
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +9 -0
- package/src/config/bundled-skills/document/SKILL.md +18 -0
- package/src/config/bundled-skills/document/TOOLS.json +53 -0
- package/src/config/bundled-skills/document/tools/document-create.ts +9 -0
- package/src/config/bundled-skills/document/tools/document-update.ts +9 -0
- package/src/config/bundled-skills/doordash/SKILL.md +163 -0
- package/src/config/bundled-skills/followups/SKILL.md +32 -0
- package/src/config/bundled-skills/followups/TOOLS.json +100 -0
- package/src/config/bundled-skills/followups/tools/followup-create.ts +9 -0
- package/src/config/bundled-skills/followups/tools/followup-list.ts +9 -0
- package/src/config/bundled-skills/followups/tools/followup-resolve.ts +9 -0
- package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -24
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -1
- package/src/config/bundled-skills/playbooks/SKILL.md +31 -0
- package/src/config/bundled-skills/playbooks/TOOLS.json +126 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +9 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +9 -0
- package/src/config/bundled-skills/reminder/SKILL.md +20 -0
- package/src/config/bundled-skills/reminder/TOOLS.json +67 -0
- package/src/config/bundled-skills/reminder/tools/reminder-cancel.ts +9 -0
- package/src/config/bundled-skills/reminder/tools/reminder-create.ts +9 -0
- package/src/config/bundled-skills/reminder/tools/reminder-list.ts +9 -0
- package/src/config/bundled-skills/schedule/SKILL.md +74 -0
- package/src/config/bundled-skills/schedule/TOOLS.json +135 -0
- package/src/config/bundled-skills/schedule/tools/schedule-create.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-delete.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-list.ts +9 -0
- package/src/config/bundled-skills/schedule/tools/schedule-update.ts +9 -0
- package/src/config/bundled-skills/subagent/SKILL.md +25 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +107 -0
- package/src/config/bundled-skills/subagent/tools/subagent-abort.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-message.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-read.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-spawn.ts +9 -0
- package/src/config/bundled-skills/subagent/tools/subagent-status.ts +9 -0
- package/src/config/bundled-skills/tasks/SKILL.md +28 -0
- package/src/config/bundled-skills/tasks/TOOLS.json +256 -0
- package/src/config/bundled-skills/tasks/tools/task-delete.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-add.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-show.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list-update.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-list.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-run.ts +9 -0
- package/src/config/bundled-skills/tasks/tools/task-save.ts +9 -0
- package/src/config/bundled-skills/twitter/SKILL.md +134 -0
- package/src/config/bundled-skills/watcher/SKILL.md +27 -0
- package/src/config/bundled-skills/watcher/TOOLS.json +147 -0
- package/src/config/bundled-skills/watcher/tools/watcher-create.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-list.ts +9 -0
- package/src/config/bundled-skills/watcher/tools/watcher-update.ts +9 -0
- package/src/config/defaults.ts +44 -0
- package/src/config/loader.ts +4 -1
- package/src/config/schema.ts +218 -1
- package/src/config/system-prompt.ts +100 -6
- package/src/config/templates/IDENTITY.md +7 -0
- package/src/config/types.ts +5 -0
- package/src/contacts/contact-store.ts +4 -4
- package/src/daemon/assistant-attachments.ts +10 -0
- package/src/daemon/classifier.ts +3 -1
- package/src/daemon/computer-use-session.ts +3 -1
- package/src/daemon/date-context.ts +136 -0
- package/src/daemon/handlers/apps.ts +16 -1
- package/src/daemon/handlers/browser.ts +54 -0
- package/src/daemon/handlers/computer-use.ts +7 -1
- package/src/daemon/handlers/config.ts +192 -4
- package/src/daemon/handlers/diagnostics.ts +5 -1
- package/src/daemon/handlers/documents.ts +18 -29
- package/src/daemon/handlers/home-base.ts +5 -1
- package/src/daemon/handlers/index.ts +40 -271
- package/src/daemon/handlers/misc.ts +9 -1
- package/src/daemon/handlers/publish.ts +6 -1
- package/src/daemon/handlers/sessions.ts +65 -12
- package/src/daemon/handlers/shared.ts +36 -1
- package/src/daemon/handlers/signing.ts +37 -0
- package/src/daemon/handlers/skills.ts +20 -6
- package/src/daemon/handlers/subagents.ts +8 -3
- package/src/daemon/handlers/twitter-auth.ts +169 -0
- package/src/daemon/handlers/work-items.ts +495 -39
- package/src/daemon/ipc-contract-inventory.json +40 -4
- package/src/daemon/ipc-contract.ts +185 -37
- package/src/daemon/ipc-protocol.ts +7 -2
- package/src/daemon/lifecycle.ts +48 -5
- package/src/daemon/main.ts +10 -4
- package/src/daemon/ride-shotgun-handler.ts +74 -10
- package/src/daemon/server.ts +144 -29
- package/src/daemon/session-agent-loop.ts +887 -0
- package/src/daemon/session-attachments.ts +28 -5
- package/src/daemon/session-error.ts +24 -3
- package/src/daemon/session-lifecycle.ts +147 -0
- package/src/daemon/session-media-retry.ts +147 -0
- package/src/daemon/session-messaging.ts +145 -0
- package/src/daemon/session-notifiers.ts +164 -0
- package/src/daemon/session-process.ts +2 -2
- package/src/daemon/session-queue-manager.ts +1 -0
- package/src/daemon/session-runtime-assembly.ts +52 -0
- package/src/daemon/session-skill-tools.ts +124 -5
- package/src/daemon/session-slash.ts +3 -0
- package/src/daemon/session-surfaces.ts +77 -2
- package/src/daemon/session-tool-setup.ts +222 -2
- package/src/daemon/session-usage.ts +0 -2
- package/src/daemon/session.ts +114 -1365
- package/src/daemon/video-thumbnail.ts +60 -0
- package/src/doordash/client.ts +121 -27
- package/src/doordash/queries.ts +1 -2
- package/src/export/formatter.ts +3 -1
- package/src/followups/followup-store.ts +4 -2
- package/src/followups/types.ts +6 -0
- package/src/hooks/templates.ts +1 -1
- package/src/index.ts +32 -1151
- package/src/media/gemini-image-service.ts +1 -1
- package/src/memory/attachments-store.ts +28 -83
- package/src/memory/channel-delivery-store.ts +7 -21
- package/src/memory/clarification-resolver.ts +6 -5
- package/src/memory/contradiction-checker.ts +3 -2
- package/src/memory/conversation-key-store.ts +10 -29
- package/src/memory/conversation-store.ts +2 -1
- package/src/memory/db.ts +362 -2
- package/src/memory/entity-extractor.ts +6 -3
- package/src/memory/items-extractor.ts +5 -4
- package/src/memory/jobs-store.ts +3 -2
- package/src/memory/llm-usage-store.ts +1 -2
- package/src/memory/runs-store.ts +1 -2
- package/src/memory/schema.ts +65 -2
- package/src/messaging/style-analyzer.ts +3 -2
- package/src/messaging/thread-summarizer.ts +8 -12
- package/src/messaging/triage-engine.ts +4 -2
- package/src/providers/openrouter/client.ts +20 -0
- package/src/providers/registry.ts +8 -0
- package/src/runtime/http-server.ts +277 -25
- package/src/runtime/http-types.ts +0 -2
- package/src/runtime/routes/attachment-routes.ts +5 -6
- package/src/runtime/routes/call-routes.ts +140 -0
- package/src/runtime/routes/channel-routes.ts +12 -19
- package/src/runtime/routes/conversation-routes.ts +5 -9
- package/src/runtime/routes/run-routes.ts +4 -8
- package/src/runtime/run-orchestrator.ts +39 -6
- package/src/schedule/recurrence-engine.ts +138 -0
- package/src/schedule/recurrence-types.ts +67 -0
- package/src/schedule/schedule-store.ts +102 -57
- package/src/schedule/scheduler.ts +9 -6
- package/src/security/oauth2.ts +29 -4
- package/src/security/secret-allowlist.ts +46 -0
- package/src/skills/clawhub.ts +1 -1
- package/src/subagent/manager.ts +40 -8
- package/src/swarm/backend-claude-code.ts +64 -9
- package/src/swarm/worker-prompts.ts +2 -1
- package/src/tasks/SPEC.md +34 -28
- package/src/tasks/ephemeral-permissions.ts +16 -7
- package/src/tasks/task-compiler.ts +5 -4
- package/src/tasks/task-runner.ts +10 -5
- package/src/tasks/task-scheduler.ts +1 -1
- package/src/tasks/tool-sanitizer.ts +36 -0
- package/src/tools/assets/search.ts +4 -4
- package/src/tools/browser/api-map.ts +220 -0
- package/src/tools/browser/auto-navigate.ts +270 -0
- package/src/tools/browser/browser-execution.ts +2 -1
- package/src/tools/browser/browser-manager.ts +2 -2
- package/src/tools/browser/network-recorder.ts +5 -4
- package/src/tools/browser/x-auto-navigate.ts +207 -0
- package/src/tools/calls/call-end.ts +67 -0
- package/src/tools/calls/call-start.ts +73 -0
- package/src/tools/calls/call-status.ts +81 -0
- package/src/tools/claude-code/claude-code.ts +77 -11
- package/src/tools/contacts/contact-merge.ts +46 -78
- package/src/tools/contacts/contact-search.ts +35 -79
- package/src/tools/contacts/contact-upsert.ts +35 -108
- package/src/tools/credentials/vault.ts +21 -5
- package/src/tools/document/document-tool.ts +71 -144
- package/src/tools/executor.ts +129 -10
- package/src/tools/followups/followup_create.ts +46 -88
- package/src/tools/followups/followup_list.ts +34 -74
- package/src/tools/followups/followup_resolve.ts +31 -66
- package/src/tools/host-terminal/cli-discover.ts +2 -1
- package/src/tools/host-terminal/host-shell.ts +10 -0
- package/src/tools/memory/handlers.ts +5 -4
- package/src/tools/network/__tests__/web-search.test.ts +427 -0
- package/src/tools/network/script-proxy/__tests__/logging.test.ts +248 -0
- package/src/tools/network/script-proxy/__tests__/policy.test.ts +234 -0
- package/src/tools/network/script-proxy/__tests__/router.test.ts +76 -0
- package/src/tools/network/web-fetch.ts +18 -6
- package/src/tools/playbooks/index.ts +4 -5
- package/src/tools/playbooks/playbook-create.ts +3 -47
- package/src/tools/playbooks/playbook-delete.ts +1 -25
- package/src/tools/playbooks/playbook-list.ts +1 -28
- package/src/tools/playbooks/playbook-update.ts +3 -51
- package/src/tools/registry.ts +2 -4
- package/src/tools/reminder/reminder.ts +5 -78
- package/src/tools/schedule/create.ts +69 -74
- package/src/tools/schedule/delete.ts +21 -47
- package/src/tools/schedule/list.ts +55 -74
- package/src/tools/schedule/update.ts +77 -84
- package/src/tools/subagent/abort.ts +29 -58
- package/src/tools/subagent/message.ts +30 -63
- package/src/tools/subagent/read.ts +53 -84
- package/src/tools/subagent/spawn.ts +43 -82
- package/src/tools/subagent/status.ts +42 -71
- package/src/tools/swarm/delegate.ts +2 -1
- package/src/tools/tasks/index.ts +8 -6
- package/src/tools/tasks/task-delete.ts +69 -56
- package/src/tools/tasks/task-list.ts +31 -52
- package/src/tools/tasks/task-run.ts +74 -102
- package/src/tools/tasks/task-save.ts +33 -65
- package/src/tools/tasks/work-item-enqueue.ts +192 -134
- package/src/tools/tasks/work-item-list.ts +33 -78
- package/src/tools/tasks/work-item-remove.ts +60 -0
- package/src/tools/tasks/work-item-update.ts +114 -0
- package/src/tools/terminal/backends/native.ts +3 -1
- package/src/tools/tool-manifest.ts +20 -74
- package/src/tools/types.ts +6 -0
- package/src/tools/ui-surface/definitions.ts +6 -1
- package/src/tools/watch/screen-watch.ts +3 -1
- package/src/tools/watcher/create.ts +52 -98
- package/src/tools/watcher/delete.ts +20 -46
- package/src/tools/watcher/digest.ts +36 -70
- package/src/tools/watcher/list.ts +49 -79
- package/src/tools/watcher/update.ts +45 -91
- package/src/twitter/client.ts +690 -0
- package/src/twitter/session.ts +91 -0
- package/src/usage/types.ts +0 -1
- package/src/util/truncate.ts +6 -0
- package/src/watcher/providers/slack.ts +2 -1
- package/src/watcher/watcher-store.ts +3 -2
- package/src/work-items/work-item-store.ts +236 -2
- package/src/workspace/commit-message-enrichment-service.ts +284 -0
- package/src/workspace/commit-message-provider.ts +95 -0
- package/src/workspace/git-service.ts +272 -52
- package/src/workspace/heartbeat-service.ts +70 -13
- package/src/workspace/provider-commit-message-generator.ts +242 -0
- package/src/workspace/turn-commit.ts +100 -51
- package/src/tools/contacts/index.ts +0 -4
- package/src/tools/document/index.ts +0 -5
- package/src/tools/followups/index.ts +0 -3
- package/src/tools/subagent/index.ts +0 -5
- /package/src/__tests__/{memory-context-benchmark.test.ts → memory-context-benchmark.benchmark.test.ts} +0 -0
|
@@ -2,21 +2,30 @@ import * as net from 'node:net';
|
|
|
2
2
|
import type {
|
|
3
3
|
WorkItemsListRequest,
|
|
4
4
|
WorkItemGetRequest,
|
|
5
|
-
WorkItemCreateRequest,
|
|
6
5
|
WorkItemUpdateRequest,
|
|
7
6
|
WorkItemCompleteRequest,
|
|
7
|
+
WorkItemDeleteRequest,
|
|
8
8
|
WorkItemRunTaskRequest,
|
|
9
|
+
WorkItemOutputRequest,
|
|
10
|
+
WorkItemPreflightRequest,
|
|
11
|
+
WorkItemApprovePermissionsRequest,
|
|
12
|
+
WorkItemCancelRequest,
|
|
9
13
|
} from '../ipc-protocol.js';
|
|
10
|
-
import { log, type HandlerContext } from './shared.js';
|
|
14
|
+
import { log, defineHandlers, type HandlerContext } from './shared.js';
|
|
15
|
+
import { getSubagentManager } from '../../subagent/index.js';
|
|
11
16
|
import {
|
|
12
|
-
|
|
17
|
+
deleteWorkItem,
|
|
13
18
|
getWorkItem,
|
|
14
19
|
listWorkItems,
|
|
15
20
|
updateWorkItem,
|
|
16
21
|
type WorkItemStatus,
|
|
17
22
|
} from '../../work-items/work-item-store.js';
|
|
18
|
-
import { getTask } from '../../tasks/task-store.js';
|
|
23
|
+
import { getTask, getTaskRun } from '../../tasks/task-store.js';
|
|
19
24
|
import { runTask } from '../../tasks/task-runner.js';
|
|
25
|
+
import { getMessages } from '../../memory/conversation-store.js';
|
|
26
|
+
import { classifyRisk, check } from '../../permissions/checker.js';
|
|
27
|
+
import { truncate } from '../../util/truncate.js';
|
|
28
|
+
import { sanitizeToolList, getRegisteredToolNames, getToolDescription } from '../../tasks/tool-sanitizer.js';
|
|
20
29
|
|
|
21
30
|
export function handleWorkItemsList(
|
|
22
31
|
msg: WorkItemsListRequest,
|
|
@@ -36,34 +45,20 @@ export function handleWorkItemGet(
|
|
|
36
45
|
ctx.send(socket, { type: 'work_item_get_response', item });
|
|
37
46
|
}
|
|
38
47
|
|
|
39
|
-
export function handleWorkItemCreate(
|
|
40
|
-
msg: WorkItemCreateRequest,
|
|
41
|
-
socket: net.Socket,
|
|
42
|
-
ctx: HandlerContext,
|
|
43
|
-
): void {
|
|
44
|
-
const task = getTask(msg.taskId);
|
|
45
|
-
if (!task) {
|
|
46
|
-
ctx.send(socket, { type: 'error', message: `Task not found: ${msg.taskId}` });
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
const item = createWorkItem({
|
|
50
|
-
taskId: msg.taskId,
|
|
51
|
-
title: msg.title ?? task.title,
|
|
52
|
-
notes: msg.notes,
|
|
53
|
-
priorityTier: msg.priorityTier,
|
|
54
|
-
sortIndex: msg.sortIndex,
|
|
55
|
-
});
|
|
56
|
-
ctx.send(socket, { type: 'work_item_create_response', item });
|
|
57
|
-
|
|
58
|
-
// Notify all connected clients so open Task Queue views refresh immediately
|
|
59
|
-
broadcastWorkItemStatus(ctx, item.id);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
48
|
export function handleWorkItemUpdate(
|
|
63
49
|
msg: WorkItemUpdateRequest,
|
|
64
50
|
socket: net.Socket,
|
|
65
51
|
ctx: HandlerContext,
|
|
66
52
|
): void {
|
|
53
|
+
// Don't allow overwriting a cancelled status (e.g. from a late chat-completion observer)
|
|
54
|
+
if (msg.status !== undefined) {
|
|
55
|
+
const existing = getWorkItem(msg.id);
|
|
56
|
+
if (existing?.status === 'cancelled' && msg.status !== 'cancelled') {
|
|
57
|
+
ctx.send(socket, { type: 'work_item_update_response', item: existing });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
67
62
|
const updates: Record<string, unknown> = {};
|
|
68
63
|
if (msg.title !== undefined) updates.title = msg.title;
|
|
69
64
|
if (msg.notes !== undefined) updates.notes = msg.notes;
|
|
@@ -78,6 +73,7 @@ export function handleWorkItemUpdate(
|
|
|
78
73
|
// (e.g. priority/sort changes made by one client are reflected everywhere)
|
|
79
74
|
if (item) {
|
|
80
75
|
broadcastWorkItemStatus(ctx, item.id);
|
|
76
|
+
ctx.broadcast({ type: 'tasks_changed' });
|
|
81
77
|
}
|
|
82
78
|
}
|
|
83
79
|
|
|
@@ -86,6 +82,18 @@ export function handleWorkItemComplete(
|
|
|
86
82
|
socket: net.Socket,
|
|
87
83
|
ctx: HandlerContext,
|
|
88
84
|
): void {
|
|
85
|
+
// Only allow completion from the 'awaiting_review' state — this ensures
|
|
86
|
+
// items go through the full run lifecycle before being marked done.
|
|
87
|
+
const existing = getWorkItem(msg.id);
|
|
88
|
+
if (!existing) {
|
|
89
|
+
ctx.send(socket, { type: 'error', message: `Work item not found: ${msg.id}` });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (existing.status !== 'awaiting_review') {
|
|
93
|
+
ctx.send(socket, { type: 'error', message: `Cannot complete work item: status is '${existing.status}', expected 'awaiting_review'` });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
89
97
|
const item = updateWorkItem(msg.id, { status: 'done' }) ?? null;
|
|
90
98
|
ctx.send(socket, { type: 'work_item_update_response', item });
|
|
91
99
|
if (item) {
|
|
@@ -102,9 +110,25 @@ export function handleWorkItemComplete(
|
|
|
102
110
|
updatedAt: item.updatedAt,
|
|
103
111
|
},
|
|
104
112
|
});
|
|
113
|
+
ctx.broadcast({ type: 'tasks_changed' });
|
|
105
114
|
}
|
|
106
115
|
}
|
|
107
116
|
|
|
117
|
+
export function handleWorkItemDelete(
|
|
118
|
+
msg: WorkItemDeleteRequest,
|
|
119
|
+
socket: net.Socket,
|
|
120
|
+
ctx: HandlerContext,
|
|
121
|
+
): void {
|
|
122
|
+
const existing = getWorkItem(msg.id);
|
|
123
|
+
if (!existing) {
|
|
124
|
+
ctx.send(socket, { type: 'work_item_delete_response', id: msg.id, success: false });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
deleteWorkItem(msg.id);
|
|
128
|
+
ctx.send(socket, { type: 'work_item_delete_response', id: msg.id, success: true });
|
|
129
|
+
ctx.broadcast({ type: 'tasks_changed' });
|
|
130
|
+
}
|
|
131
|
+
|
|
108
132
|
function broadcastWorkItemStatus(ctx: HandlerContext, id: string): void {
|
|
109
133
|
const item = getWorkItem(id);
|
|
110
134
|
if (item) {
|
|
@@ -124,6 +148,212 @@ function broadcastWorkItemStatus(ctx: HandlerContext, id: string): void {
|
|
|
124
148
|
}
|
|
125
149
|
}
|
|
126
150
|
|
|
151
|
+
/** Extract plain text from a message content string (handles JSON content block arrays). */
|
|
152
|
+
function extractTextFromContent(content: string): string {
|
|
153
|
+
try {
|
|
154
|
+
const parsed = JSON.parse(content);
|
|
155
|
+
if (Array.isArray(parsed)) {
|
|
156
|
+
return parsed
|
|
157
|
+
.filter((b: { type: string }) => b.type === 'text')
|
|
158
|
+
.map((b: { text: string }) => b.text)
|
|
159
|
+
.join('\n');
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
// Plain text content — use as-is
|
|
163
|
+
}
|
|
164
|
+
return content;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Extract tool_result blocks from a user message's content. */
|
|
168
|
+
function extractToolResults(content: string): Array<{ tool_use_id: string; content: string; is_error?: boolean }> {
|
|
169
|
+
try {
|
|
170
|
+
const parsed = JSON.parse(content);
|
|
171
|
+
if (Array.isArray(parsed)) {
|
|
172
|
+
return parsed
|
|
173
|
+
.filter((b: { type: string }) => b.type === 'tool_result')
|
|
174
|
+
.map((b: { tool_use_id: string; content?: string | Array<{ type: string; text?: string }>; is_error?: boolean }) => {
|
|
175
|
+
let text = '';
|
|
176
|
+
if (typeof b.content === 'string') {
|
|
177
|
+
text = b.content;
|
|
178
|
+
} else if (Array.isArray(b.content)) {
|
|
179
|
+
text = b.content
|
|
180
|
+
.filter((c) => c.type === 'text' && c.text)
|
|
181
|
+
.map((c) => c.text!)
|
|
182
|
+
.join('\n');
|
|
183
|
+
}
|
|
184
|
+
return { tool_use_id: b.tool_use_id, content: text, is_error: b.is_error };
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
} catch {
|
|
188
|
+
// Not JSON — no tool_result blocks
|
|
189
|
+
}
|
|
190
|
+
return [];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Build highlights from tool outcomes in the conversation. Scans for
|
|
195
|
+
* tool_use (assistant) and tool_result (user) pairs, extracting concrete
|
|
196
|
+
* outcomes like errors, file paths, and URLs.
|
|
197
|
+
*/
|
|
198
|
+
function extractToolHighlights(
|
|
199
|
+
msgs: Array<{ role: string; content: string }>,
|
|
200
|
+
maxHighlights: number,
|
|
201
|
+
): string[] {
|
|
202
|
+
const highlights: string[] = [];
|
|
203
|
+
|
|
204
|
+
// Build a map of tool_use_id -> tool name from assistant messages
|
|
205
|
+
const toolNameById = new Map<string, string>();
|
|
206
|
+
for (const m of msgs) {
|
|
207
|
+
if (m.role !== 'assistant') continue;
|
|
208
|
+
try {
|
|
209
|
+
const parsed = JSON.parse(m.content);
|
|
210
|
+
if (Array.isArray(parsed)) {
|
|
211
|
+
for (const block of parsed) {
|
|
212
|
+
if (block.type === 'tool_use' && block.id && block.name) {
|
|
213
|
+
toolNameById.set(block.id, block.name);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
} catch { /* skip */ }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Scan tool_result messages in reverse order (most recent first)
|
|
221
|
+
for (let i = msgs.length - 1; i >= 0 && highlights.length < maxHighlights; i--) {
|
|
222
|
+
const m = msgs[i];
|
|
223
|
+
if (m.role !== 'user') continue;
|
|
224
|
+
|
|
225
|
+
const results = extractToolResults(m.content);
|
|
226
|
+
for (const result of results) {
|
|
227
|
+
if (highlights.length >= maxHighlights) break;
|
|
228
|
+
|
|
229
|
+
const toolName = toolNameById.get(result.tool_use_id) ?? 'tool';
|
|
230
|
+
const resultText = result.content.trim();
|
|
231
|
+
|
|
232
|
+
if (result.is_error) {
|
|
233
|
+
// Always surface errors
|
|
234
|
+
const errorSnippet = truncate(resultText, 200, '...');
|
|
235
|
+
highlights.push(`- ${toolName}: Error — ${errorSnippet}`);
|
|
236
|
+
} else if (resultText) {
|
|
237
|
+
// Extract notable signal from successful results: file paths, URLs, or
|
|
238
|
+
// a short summary of what happened
|
|
239
|
+
const firstLine = resultText.split('\n')[0].trim();
|
|
240
|
+
if (firstLine.length > 0 && firstLine.length <= 200) {
|
|
241
|
+
highlights.push(`- ${toolName}: ${firstLine}`);
|
|
242
|
+
} else if (firstLine.length > 200) {
|
|
243
|
+
highlights.push(`- ${toolName}: ${truncate(firstLine, 200, '...')}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return highlights;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function handleWorkItemOutput(
|
|
253
|
+
msg: WorkItemOutputRequest,
|
|
254
|
+
socket: net.Socket,
|
|
255
|
+
ctx: HandlerContext,
|
|
256
|
+
): void {
|
|
257
|
+
try {
|
|
258
|
+
const workItem = getWorkItem(msg.id);
|
|
259
|
+
if (!workItem) {
|
|
260
|
+
ctx.send(socket, { type: 'work_item_output_response', id: msg.id, success: false, error: 'Work item not found' });
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Use the task run's conversationId as the authoritative source. This
|
|
265
|
+
// ensures we read from the actual run's conversation, not stale references
|
|
266
|
+
// on the work item.
|
|
267
|
+
let conversationId: string | null = null;
|
|
268
|
+
let completedAt: number | null = null;
|
|
269
|
+
|
|
270
|
+
if (workItem.lastRunId) {
|
|
271
|
+
const run = getTaskRun(workItem.lastRunId);
|
|
272
|
+
if (run) {
|
|
273
|
+
conversationId = run.conversationId;
|
|
274
|
+
completedAt = run.finishedAt != null ? Math.floor(run.finishedAt / 1000) : null;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Fall back to the work item's stored conversationId if the run lookup
|
|
279
|
+
// didn't yield one (e.g. run record was deleted but work item still has
|
|
280
|
+
// the reference).
|
|
281
|
+
if (!conversationId) {
|
|
282
|
+
conversationId = workItem.lastRunConversationId;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (!conversationId) {
|
|
286
|
+
ctx.send(socket, { type: 'work_item_output_response', id: msg.id, success: false, error: 'This task has not been run yet. No output is available.' });
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
let summary = '';
|
|
291
|
+
let highlights: string[] = [];
|
|
292
|
+
|
|
293
|
+
const msgs = getMessages(conversationId);
|
|
294
|
+
|
|
295
|
+
// Find the last assistant message with text content (not tool calls).
|
|
296
|
+
// Skip messages that are purely about task management rather than
|
|
297
|
+
// reporting what the run actually did.
|
|
298
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
299
|
+
const m = msgs[i];
|
|
300
|
+
if (m.role !== 'assistant') continue;
|
|
301
|
+
|
|
302
|
+
const text = extractTextFromContent(m.content);
|
|
303
|
+
if (!text.trim()) continue;
|
|
304
|
+
|
|
305
|
+
summary = truncate(text, 2000, '');
|
|
306
|
+
|
|
307
|
+
// Extract bullet points from the assistant's prose
|
|
308
|
+
const lines = text.split('\n');
|
|
309
|
+
for (const line of lines) {
|
|
310
|
+
const trimmed = line.trim();
|
|
311
|
+
if ((trimmed.startsWith('-') || trimmed.startsWith('*')) && trimmed.length > 2) {
|
|
312
|
+
highlights.push(trimmed);
|
|
313
|
+
if (highlights.length >= 5) break;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// If we didn't get enough highlights from the assistant prose, supplement
|
|
320
|
+
// with concrete tool outcomes from the conversation.
|
|
321
|
+
if (highlights.length < 5) {
|
|
322
|
+
const toolHighlights = extractToolHighlights(msgs, 5 - highlights.length);
|
|
323
|
+
highlights = [...highlights, ...toolHighlights];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// If there's no assistant summary at all, synthesize one from tool results
|
|
327
|
+
// so the user still sees what happened.
|
|
328
|
+
if (!summary && msgs.length > 0) {
|
|
329
|
+
const toolHighlights = extractToolHighlights(msgs, 10);
|
|
330
|
+
if (toolHighlights.length > 0) {
|
|
331
|
+
summary = 'Task completed. Tool outcomes:\n' + toolHighlights.join('\n');
|
|
332
|
+
// Use the tool highlights as the main highlights too
|
|
333
|
+
highlights = toolHighlights.slice(0, 5);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
ctx.send(socket, {
|
|
338
|
+
type: 'work_item_output_response',
|
|
339
|
+
id: msg.id,
|
|
340
|
+
success: true,
|
|
341
|
+
output: {
|
|
342
|
+
title: workItem.title,
|
|
343
|
+
status: workItem.lastRunStatus ?? workItem.status,
|
|
344
|
+
runId: workItem.lastRunId,
|
|
345
|
+
conversationId,
|
|
346
|
+
completedAt,
|
|
347
|
+
summary,
|
|
348
|
+
highlights,
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
} catch (err) {
|
|
352
|
+
log.error({ err, workItemId: msg.id }, 'handleWorkItemOutput failed');
|
|
353
|
+
ctx.send(socket, { type: 'work_item_output_response', id: msg.id, success: false, error: 'Failed to load task output' });
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
127
357
|
export async function handleWorkItemRunTask(
|
|
128
358
|
msg: WorkItemRunTaskRequest,
|
|
129
359
|
socket: net.Socket,
|
|
@@ -131,10 +361,58 @@ export async function handleWorkItemRunTask(
|
|
|
131
361
|
): Promise<void> {
|
|
132
362
|
const workItem = getWorkItem(msg.id);
|
|
133
363
|
if (!workItem) {
|
|
134
|
-
ctx.send(socket, { type: 'work_item_run_task_response', id: msg.id, lastRunId: '', success: false, error: 'Work item not found' });
|
|
364
|
+
ctx.send(socket, { type: 'work_item_run_task_response', id: msg.id, lastRunId: '', success: false, error: 'Work item not found', errorCode: 'not_found' });
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (workItem.status === 'running') {
|
|
369
|
+
ctx.send(socket, { type: 'work_item_run_task_response', id: msg.id, lastRunId: workItem.lastRunId ?? '', success: false, error: 'Work item is already running', errorCode: 'already_running' });
|
|
135
370
|
return;
|
|
136
371
|
}
|
|
137
372
|
|
|
373
|
+
const NON_RUNNABLE_STATUSES: readonly string[] = ['archived'];
|
|
374
|
+
if (NON_RUNNABLE_STATUSES.includes(workItem.status)) {
|
|
375
|
+
ctx.send(socket, { type: 'work_item_run_task_response', id: msg.id, lastRunId: workItem.lastRunId ?? '', success: false, error: `Work item has status '${workItem.status}' and cannot be run`, errorCode: 'invalid_status' });
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const task = getTask(workItem.taskId);
|
|
380
|
+
if (!task) {
|
|
381
|
+
ctx.send(socket, { type: 'work_item_run_task_response', id: msg.id, lastRunId: '', success: false, error: `Associated task not found: ${workItem.taskId}`, errorCode: 'no_task' });
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Compute required tools using the same resolution logic as preflight:
|
|
386
|
+
// work-item snapshot first, then task template, then all registered tools.
|
|
387
|
+
let requiredTools: string[];
|
|
388
|
+
if (workItem.requiredTools !== null && workItem.requiredTools !== undefined) {
|
|
389
|
+
requiredTools = sanitizeToolList(JSON.parse(workItem.requiredTools));
|
|
390
|
+
} else {
|
|
391
|
+
requiredTools = task.requiredTools
|
|
392
|
+
? sanitizeToolList(JSON.parse(task.requiredTools))
|
|
393
|
+
: getRegisteredToolNames();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Permission checkpoint: if the task requires tools, verify all have been approved.
|
|
397
|
+
// Empty required tools means no approvals needed.
|
|
398
|
+
let approvedTools: string[] | undefined;
|
|
399
|
+
if (requiredTools.length > 0) {
|
|
400
|
+
approvedTools = workItem.approvedTools ? JSON.parse(workItem.approvedTools) : undefined;
|
|
401
|
+
const approvedSet = new Set<string>(approvedTools ?? []);
|
|
402
|
+
const missingApprovals = requiredTools.filter((t) => !approvedSet.has(t));
|
|
403
|
+
if (missingApprovals.length > 0) {
|
|
404
|
+
ctx.send(socket, {
|
|
405
|
+
type: 'work_item_run_task_response',
|
|
406
|
+
id: msg.id,
|
|
407
|
+
lastRunId: '',
|
|
408
|
+
success: false,
|
|
409
|
+
error: 'Required tool permissions have not been approved. Run preflight first.',
|
|
410
|
+
errorCode: 'permission_required',
|
|
411
|
+
});
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
138
416
|
// Set status to running
|
|
139
417
|
updateWorkItem(msg.id, { status: 'running' });
|
|
140
418
|
|
|
@@ -143,34 +421,212 @@ export async function handleWorkItemRunTask(
|
|
|
143
421
|
|
|
144
422
|
// Broadcast the running state
|
|
145
423
|
broadcastWorkItemStatus(ctx, msg.id);
|
|
424
|
+
ctx.broadcast({ type: 'tasks_changed' });
|
|
146
425
|
|
|
147
|
-
// Execute task asynchronously — create a session
|
|
426
|
+
// Execute task asynchronously — lazily create a session inside the callback
|
|
427
|
+
// using the conversationId provided by runTask, so the session references
|
|
428
|
+
// the conversation that was actually inserted into the database.
|
|
148
429
|
try {
|
|
149
|
-
|
|
430
|
+
let session: Awaited<ReturnType<typeof ctx.getOrCreateSession>> | null = null;
|
|
150
431
|
const result = await runTask(
|
|
151
|
-
{ taskId: workItem.taskId, workingDir: process.cwd() },
|
|
152
|
-
async (
|
|
432
|
+
{ taskId: workItem.taskId, workingDir: process.cwd(), approvedTools },
|
|
433
|
+
async (conversationId, message, taskRunId) => {
|
|
434
|
+
if (!session) {
|
|
435
|
+
// Store conversationId on the work item immediately so the cancel
|
|
436
|
+
// handler can locate the session while the task is still running.
|
|
437
|
+
updateWorkItem(msg.id, { lastRunConversationId: conversationId });
|
|
438
|
+
session = await ctx.getOrCreateSession(conversationId);
|
|
439
|
+
|
|
440
|
+
// Notify clients so they can create a visible chat thread for this task run
|
|
441
|
+
ctx.broadcast({
|
|
442
|
+
type: 'task_run_thread_created',
|
|
443
|
+
conversationId,
|
|
444
|
+
workItemId: msg.id,
|
|
445
|
+
title: workItem.title,
|
|
446
|
+
});
|
|
447
|
+
// Wire the taskRunId so the executor can retrieve ephemeral permission rules
|
|
448
|
+
(session as unknown as { taskRunId?: string }).taskRunId = taskRunId;
|
|
449
|
+
// Prevent interactive clients from rebinding to this session mid-run
|
|
450
|
+
(session as unknown as { headlessLock: boolean }).headlessLock = true;
|
|
451
|
+
}
|
|
153
452
|
await session.processMessage(message, [], (event) => {
|
|
154
453
|
ctx.broadcast(event);
|
|
155
454
|
});
|
|
156
455
|
},
|
|
157
456
|
);
|
|
158
457
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
458
|
+
// Release the headless lock now that the task run is done
|
|
459
|
+
if (session) {
|
|
460
|
+
(session as unknown as { headlessLock: boolean }).headlessLock = false;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Don't overwrite cancelled status — the cancel handler already set it
|
|
464
|
+
const current = getWorkItem(msg.id);
|
|
465
|
+
if (current?.status !== 'cancelled') {
|
|
466
|
+
const finalStatus: WorkItemStatus = result.status === 'completed' ? 'awaiting_review' : 'failed';
|
|
467
|
+
updateWorkItem(msg.id, {
|
|
468
|
+
status: finalStatus,
|
|
469
|
+
lastRunId: result.taskRunId,
|
|
470
|
+
lastRunConversationId: result.conversationId,
|
|
471
|
+
lastRunStatus: result.status,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
166
474
|
|
|
167
475
|
broadcastWorkItemStatus(ctx, msg.id);
|
|
476
|
+
ctx.broadcast({ type: 'tasks_changed' });
|
|
168
477
|
} catch (err) {
|
|
478
|
+
// Release the headless lock on failure
|
|
479
|
+
if (session) {
|
|
480
|
+
(session as unknown as { headlessLock: boolean }).headlessLock = false;
|
|
481
|
+
}
|
|
169
482
|
log.error({ err, workItemId: msg.id }, 'work_item_run_task failed');
|
|
170
483
|
updateWorkItem(msg.id, {
|
|
171
484
|
status: 'failed',
|
|
172
485
|
lastRunStatus: 'failed',
|
|
173
486
|
});
|
|
174
487
|
broadcastWorkItemStatus(ctx, msg.id);
|
|
488
|
+
ctx.broadcast({ type: 'tasks_changed' });
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
export async function handleWorkItemPreflight(
|
|
494
|
+
msg: WorkItemPreflightRequest,
|
|
495
|
+
socket: net.Socket,
|
|
496
|
+
ctx: HandlerContext,
|
|
497
|
+
): Promise<void> {
|
|
498
|
+
const workItem = getWorkItem(msg.id);
|
|
499
|
+
if (!workItem) {
|
|
500
|
+
ctx.send(socket, { type: 'work_item_preflight_response', id: msg.id, success: false, error: 'Work item not found' });
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Compute required tools from the work-item snapshot first; only fall
|
|
505
|
+
// back to the task template (or all registered tools) when the
|
|
506
|
+
// snapshot is null.
|
|
507
|
+
let requiredTools: string[];
|
|
508
|
+
if (workItem.requiredTools !== null && workItem.requiredTools !== undefined) {
|
|
509
|
+
requiredTools = sanitizeToolList(JSON.parse(workItem.requiredTools));
|
|
510
|
+
} else {
|
|
511
|
+
const task = getTask(workItem.taskId);
|
|
512
|
+
if (!task) {
|
|
513
|
+
ctx.send(socket, { type: 'work_item_preflight_response', id: msg.id, success: false, error: `Associated task not found: ${workItem.taskId}` });
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
requiredTools = task.requiredTools
|
|
517
|
+
? sanitizeToolList(JSON.parse(task.requiredTools))
|
|
518
|
+
: getRegisteredToolNames();
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// If the work item explicitly requires no tools, skip the dialog.
|
|
522
|
+
if (requiredTools.length === 0) {
|
|
523
|
+
ctx.send(socket, { type: 'work_item_preflight_response', id: msg.id, success: true, permissions: [] });
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// If some tools are already approved, only prompt for the missing ones.
|
|
528
|
+
// When all required tools are covered, skip the dialog entirely.
|
|
529
|
+
if (workItem.approvedTools) {
|
|
530
|
+
const approvedSet = new Set<string>(JSON.parse(workItem.approvedTools));
|
|
531
|
+
requiredTools = requiredTools.filter((t) => !approvedSet.has(t));
|
|
532
|
+
if (requiredTools.length === 0) {
|
|
533
|
+
ctx.send(socket, { type: 'work_item_preflight_response', id: msg.id, success: true, permissions: [] });
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const workingDir = process.cwd();
|
|
539
|
+
const permissions = await Promise.all(
|
|
540
|
+
requiredTools.map(async (tool) => {
|
|
541
|
+
const risk = await classifyRisk(tool, {}, workingDir);
|
|
542
|
+
const result = await check(tool, {}, workingDir);
|
|
543
|
+
return {
|
|
544
|
+
tool,
|
|
545
|
+
description: getToolDescription(tool),
|
|
546
|
+
riskLevel: risk.toLowerCase() as 'low' | 'medium' | 'high',
|
|
547
|
+
currentDecision: result.decision as 'allow' | 'deny' | 'prompt',
|
|
548
|
+
};
|
|
549
|
+
}),
|
|
550
|
+
);
|
|
551
|
+
|
|
552
|
+
ctx.send(socket, { type: 'work_item_preflight_response', id: msg.id, success: true, permissions });
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
export function handleWorkItemApprovePermissions(
|
|
556
|
+
msg: WorkItemApprovePermissionsRequest,
|
|
557
|
+
socket: net.Socket,
|
|
558
|
+
ctx: HandlerContext,
|
|
559
|
+
): void {
|
|
560
|
+
const workItem = getWorkItem(msg.id);
|
|
561
|
+
if (!workItem) {
|
|
562
|
+
ctx.send(socket, { type: 'work_item_approve_permissions_response', id: msg.id, success: false, error: 'Work item not found' });
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Merge newly approved tools with any previously approved ones so reruns
|
|
567
|
+
// that only need a subset of previously-approved tools don't require
|
|
568
|
+
// re-approval.
|
|
569
|
+
const existingApproved: string[] = workItem.approvedTools
|
|
570
|
+
? JSON.parse(workItem.approvedTools)
|
|
571
|
+
: [];
|
|
572
|
+
const newApproved = sanitizeToolList(msg.approvedTools);
|
|
573
|
+
const merged = [...new Set([...existingApproved, ...newApproved])];
|
|
574
|
+
|
|
575
|
+
updateWorkItem(msg.id, {
|
|
576
|
+
approvedTools: JSON.stringify(sanitizeToolList(merged)),
|
|
577
|
+
approvalStatus: 'approved',
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
ctx.send(socket, { type: 'work_item_approve_permissions_response', id: msg.id, success: true });
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
export function handleWorkItemCancel(
|
|
584
|
+
msg: WorkItemCancelRequest,
|
|
585
|
+
socket: net.Socket,
|
|
586
|
+
ctx: HandlerContext,
|
|
587
|
+
): void {
|
|
588
|
+
const workItem = getWorkItem(msg.id);
|
|
589
|
+
if (!workItem) {
|
|
590
|
+
ctx.send(socket, { type: 'work_item_cancel_response', id: msg.id, success: false, error: 'Work item not found' });
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (workItem.status !== 'running') {
|
|
595
|
+
ctx.send(socket, { type: 'work_item_cancel_response', id: msg.id, success: false, error: `Work item is not running (status: ${workItem.status})` });
|
|
596
|
+
return;
|
|
175
597
|
}
|
|
598
|
+
|
|
599
|
+
// Abort the session associated with this work item's current run
|
|
600
|
+
const conversationId = workItem.lastRunConversationId;
|
|
601
|
+
if (conversationId) {
|
|
602
|
+
const session = ctx.sessions.get(conversationId);
|
|
603
|
+
if (session) {
|
|
604
|
+
(session as unknown as { headlessLock: boolean }).headlessLock = false;
|
|
605
|
+
session.abort();
|
|
606
|
+
getSubagentManager().abortAllForParent(conversationId);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
updateWorkItem(msg.id, {
|
|
611
|
+
status: 'cancelled',
|
|
612
|
+
lastRunStatus: 'cancelled',
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
ctx.send(socket, { type: 'work_item_cancel_response', id: msg.id, success: true });
|
|
616
|
+
|
|
617
|
+
broadcastWorkItemStatus(ctx, msg.id);
|
|
618
|
+
ctx.broadcast({ type: 'tasks_changed' });
|
|
176
619
|
}
|
|
620
|
+
|
|
621
|
+
export const workItemHandlers = defineHandlers({
|
|
622
|
+
work_items_list: handleWorkItemsList,
|
|
623
|
+
work_item_get: handleWorkItemGet,
|
|
624
|
+
work_item_update: handleWorkItemUpdate,
|
|
625
|
+
work_item_complete: handleWorkItemComplete,
|
|
626
|
+
work_item_delete: handleWorkItemDelete,
|
|
627
|
+
work_item_run_task: handleWorkItemRunTask,
|
|
628
|
+
work_item_output: handleWorkItemOutput,
|
|
629
|
+
work_item_preflight: handleWorkItemPreflight,
|
|
630
|
+
work_item_approve_permissions: handleWorkItemApprovePermissions,
|
|
631
|
+
work_item_cancel: handleWorkItemCancel,
|
|
632
|
+
});
|