discoclaw 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.context/README.md +42 -0
- package/.context/architecture.md +58 -0
- package/.context/bot-setup.md +24 -0
- package/.context/dev.md +230 -0
- package/.context/discord.md +144 -0
- package/.context/memory.md +257 -0
- package/.context/ops.md +59 -0
- package/.context/pa-safety.md +47 -0
- package/.context/pa.md +118 -0
- package/.context/project.md +43 -0
- package/.context/runtime.md +253 -0
- package/.context/tasks.md +71 -0
- package/.context/tools.md +75 -0
- package/.env.example +88 -0
- package/.env.example.full +378 -0
- package/LICENSE +21 -0
- package/README.md +220 -0
- package/dist/beads/auto-tag.js +2 -0
- package/dist/beads/auto-tag.test.js +62 -0
- package/dist/beads/bd-cli.js +9 -0
- package/dist/beads/bd-cli.test.js +495 -0
- package/dist/beads/bead-hooks-cli.js +149 -0
- package/dist/beads/bead-sync-cli.js +5 -0
- package/dist/beads/bead-sync-cli.test.js +72 -0
- package/dist/beads/bead-sync-coordinator.js +4 -0
- package/dist/beads/bead-sync-coordinator.test.js +239 -0
- package/dist/beads/bead-sync-watcher.js +2 -0
- package/dist/beads/bead-sync-watcher.test.js +96 -0
- package/dist/beads/bead-sync.js +7 -0
- package/dist/beads/bead-sync.test.js +876 -0
- package/dist/beads/bead-thread-cache.js +8 -0
- package/dist/beads/bead-thread-cache.test.js +91 -0
- package/dist/beads/discord-sync.js +18 -0
- package/dist/beads/discord-sync.test.js +782 -0
- package/dist/beads/find-bead-by-thread.test.js +36 -0
- package/dist/beads/forum-guard.js +2 -0
- package/dist/beads/forum-guard.test.js +204 -0
- package/dist/beads/initialize.js +3 -0
- package/dist/beads/initialize.test.js +304 -0
- package/dist/beads/types.js +10 -0
- package/dist/cli/daemon-installer.js +225 -0
- package/dist/cli/daemon-installer.test.js +289 -0
- package/dist/cli/index.js +42 -0
- package/dist/cli/init-wizard.js +374 -0
- package/dist/cli/init-wizard.test.js +191 -0
- package/dist/config.js +385 -0
- package/dist/config.test.js +589 -0
- package/dist/cron/auto-tag.js +100 -0
- package/dist/cron/auto-tag.test.js +91 -0
- package/dist/cron/cadence.js +74 -0
- package/dist/cron/cadence.test.js +53 -0
- package/dist/cron/cron-sync-coordinator.js +66 -0
- package/dist/cron/cron-sync-coordinator.test.js +118 -0
- package/dist/cron/cron-sync.js +165 -0
- package/dist/cron/cron-sync.test.js +228 -0
- package/dist/cron/cron-tag-map-watcher.js +128 -0
- package/dist/cron/cron-tag-map-watcher.test.js +155 -0
- package/dist/cron/default-timezone.js +23 -0
- package/dist/cron/default-timezone.test.js +30 -0
- package/dist/cron/discord-sync.js +205 -0
- package/dist/cron/discord-sync.test.js +353 -0
- package/dist/cron/executor.js +303 -0
- package/dist/cron/executor.test.js +614 -0
- package/dist/cron/forum-sync.js +347 -0
- package/dist/cron/forum-sync.test.js +539 -0
- package/dist/cron/job-lock.js +164 -0
- package/dist/cron/job-lock.test.js +178 -0
- package/dist/cron/parser.js +68 -0
- package/dist/cron/parser.test.js +115 -0
- package/dist/cron/run-control.js +24 -0
- package/dist/cron/run-control.test.js +27 -0
- package/dist/cron/run-stats.js +265 -0
- package/dist/cron/run-stats.test.js +160 -0
- package/dist/cron/scheduler.js +97 -0
- package/dist/cron/scheduler.test.js +112 -0
- package/dist/cron/tag-map.js +47 -0
- package/dist/cron/tag-map.test.js +64 -0
- package/dist/cron/types.js +1 -0
- package/dist/discoclaw-plan-format.test.js +137 -0
- package/dist/discoclaw-recipe-format.test.js +137 -0
- package/dist/discord/abort-registry.js +70 -0
- package/dist/discord/action-categories.js +36 -0
- package/dist/discord/action-types.js +1 -0
- package/dist/discord/action-utils.js +58 -0
- package/dist/discord/action-utils.test.js +58 -0
- package/dist/discord/actions-beads.js +1 -0
- package/dist/discord/actions-beads.test.js +372 -0
- package/dist/discord/actions-bot-profile.js +107 -0
- package/dist/discord/actions-bot-profile.test.js +138 -0
- package/dist/discord/actions-channels.js +427 -0
- package/dist/discord/actions-channels.test.js +697 -0
- package/dist/discord/actions-config.js +173 -0
- package/dist/discord/actions-config.test.js +322 -0
- package/dist/discord/actions-crons.js +586 -0
- package/dist/discord/actions-crons.test.js +499 -0
- package/dist/discord/actions-defer.js +60 -0
- package/dist/discord/actions-defer.test.js +134 -0
- package/dist/discord/actions-forge.js +134 -0
- package/dist/discord/actions-forge.test.js +206 -0
- package/dist/discord/actions-guild.js +301 -0
- package/dist/discord/actions-guild.test.js +386 -0
- package/dist/discord/actions-memory.js +106 -0
- package/dist/discord/actions-memory.test.js +248 -0
- package/dist/discord/actions-messaging.js +401 -0
- package/dist/discord/actions-messaging.test.js +738 -0
- package/dist/discord/actions-moderation.js +65 -0
- package/dist/discord/actions-moderation.test.js +88 -0
- package/dist/discord/actions-plan.js +445 -0
- package/dist/discord/actions-plan.test.js +610 -0
- package/dist/discord/actions-poll.js +38 -0
- package/dist/discord/actions-poll.test.js +93 -0
- package/dist/discord/actions-tasks.js +3 -0
- package/dist/discord/actions-tasks.test.js +418 -0
- package/dist/discord/actions.js +600 -0
- package/dist/discord/actions.test.js +522 -0
- package/dist/discord/allowed-mentions.js +3 -0
- package/dist/discord/allowed-mentions.test.js +17 -0
- package/dist/discord/allowlist.js +29 -0
- package/dist/discord/allowlist.test.js +24 -0
- package/dist/discord/audit-handler.js +191 -0
- package/dist/discord/audit-handler.test.js +361 -0
- package/dist/discord/bot.js +141 -0
- package/dist/discord/channel-context.js +181 -0
- package/dist/discord/defer-scheduler.js +45 -0
- package/dist/discord/destructive-confirmation.js +128 -0
- package/dist/discord/destructive-confirmation.test.js +49 -0
- package/dist/discord/discord-plan-auto-implement.test.js +18 -0
- package/dist/discord/durable-memory.js +145 -0
- package/dist/discord/durable-memory.test.js +281 -0
- package/dist/discord/durable-write-queue.js +4 -0
- package/dist/discord/file-download.js +308 -0
- package/dist/discord/file-download.test.js +303 -0
- package/dist/discord/forge-audit-verdict.js +140 -0
- package/dist/discord/forge-auto-implement.js +80 -0
- package/dist/discord/forge-auto-implement.test.js +110 -0
- package/dist/discord/forge-commands.js +698 -0
- package/dist/discord/forge-commands.test.js +1606 -0
- package/dist/discord/forge-plan-registry.js +68 -0
- package/dist/discord/forge-plan-registry.test.js +127 -0
- package/dist/discord/forum-count-sync.js +130 -0
- package/dist/discord/forum-count-sync.test.js +200 -0
- package/dist/discord/health-command.js +98 -0
- package/dist/discord/health-command.test.js +195 -0
- package/dist/discord/help-command.js +22 -0
- package/dist/discord/help-command.test.js +49 -0
- package/dist/discord/image-download.js +201 -0
- package/dist/discord/image-download.test.js +499 -0
- package/dist/discord/inflight-replies.js +228 -0
- package/dist/discord/inflight-replies.test.js +295 -0
- package/dist/discord/json-extract.js +110 -0
- package/dist/discord/keyed-queue.js +22 -0
- package/dist/discord/memory-commands.js +85 -0
- package/dist/discord/memory-commands.test.js +159 -0
- package/dist/discord/memory-timing.integration.test.js +159 -0
- package/dist/discord/message-coordinator.js +2347 -0
- package/dist/discord/message-coordinator.onboarding.test.js +183 -0
- package/dist/discord/message-coordinator.plan-run.test.js +264 -0
- package/dist/discord/message-history.js +53 -0
- package/dist/discord/message-history.test.js +95 -0
- package/dist/discord/models-command.js +59 -0
- package/dist/discord/models-command.test.js +150 -0
- package/dist/discord/nickname.test.js +76 -0
- package/dist/discord/onboarding-completion.js +55 -0
- package/dist/discord/onboarding-completion.test.js +176 -0
- package/dist/discord/output-common.js +178 -0
- package/dist/discord/output-common.test.js +198 -0
- package/dist/discord/output-utils.js +156 -0
- package/dist/discord/parse-identity-name.test.js +129 -0
- package/dist/discord/plan-commands.js +612 -0
- package/dist/discord/plan-commands.test.js +1622 -0
- package/dist/discord/plan-manager.js +1491 -0
- package/dist/discord/plan-manager.test.js +2380 -0
- package/dist/discord/plan-parser.js +110 -0
- package/dist/discord/plan-parser.test.js +63 -0
- package/dist/discord/plan-run-phase-start.js +20 -0
- package/dist/discord/plan-run-phase-start.test.js +29 -0
- package/dist/discord/platform-message.js +45 -0
- package/dist/discord/platform-message.test.js +110 -0
- package/dist/discord/prompt-common.js +240 -0
- package/dist/discord/prompt-common.test.js +423 -0
- package/dist/discord/reaction-handler.js +691 -0
- package/dist/discord/reaction-handler.test.js +1574 -0
- package/dist/discord/reaction-prompts.js +118 -0
- package/dist/discord/reaction-prompts.test.js +253 -0
- package/dist/discord/reply-reference.js +66 -0
- package/dist/discord/reply-reference.test.js +125 -0
- package/dist/discord/restart-command.js +143 -0
- package/dist/discord/restart-command.test.js +196 -0
- package/dist/discord/runtime-utils.js +43 -0
- package/dist/discord/runtime-utils.test.js +112 -0
- package/dist/discord/session-key.js +7 -0
- package/dist/discord/session-key.test.js +13 -0
- package/dist/discord/shortterm-memory.js +166 -0
- package/dist/discord/shortterm-memory.test.js +345 -0
- package/dist/discord/shutdown-context.js +122 -0
- package/dist/discord/shutdown-context.test.js +279 -0
- package/dist/discord/startup-profile.test.js +214 -0
- package/dist/discord/status-channel.js +190 -0
- package/dist/discord/status-channel.test.js +282 -0
- package/dist/discord/status-command.js +206 -0
- package/dist/discord/status-command.test.js +341 -0
- package/dist/discord/streaming-progress.js +107 -0
- package/dist/discord/streaming-progress.test.js +93 -0
- package/dist/discord/summarizer.js +89 -0
- package/dist/discord/summarizer.test.js +245 -0
- package/dist/discord/system-bootstrap.js +396 -0
- package/dist/discord/system-bootstrap.test.js +724 -0
- package/dist/discord/thread-context.js +169 -0
- package/dist/discord/thread-context.test.js +386 -0
- package/dist/discord/tool-aware-queue.js +116 -0
- package/dist/discord/tool-aware-queue.test.js +180 -0
- package/dist/discord/update-command.js +127 -0
- package/dist/discord/update-command.test.js +275 -0
- package/dist/discord/user-errors.js +40 -0
- package/dist/discord/user-errors.test.js +31 -0
- package/dist/discord/user-turn-to-durable.js +111 -0
- package/dist/discord/user-turn-to-durable.test.js +273 -0
- package/dist/discord-followup.test.js +677 -0
- package/dist/discord.channel-context.test.js +95 -0
- package/dist/discord.fail-closed.test.js +199 -0
- package/dist/discord.health-command.integration.test.js +140 -0
- package/dist/discord.js +190 -0
- package/dist/discord.prompt-context.test.js +1431 -0
- package/dist/discord.render.test.js +621 -0
- package/dist/discord.status-wiring.test.js +187 -0
- package/dist/engine/claudeCli.js +137 -0
- package/dist/engine/types.js +1 -0
- package/dist/group-queue.js +25 -0
- package/dist/health/credential-check.js +175 -0
- package/dist/health/credential-check.test.js +401 -0
- package/dist/health/startup-healing.js +139 -0
- package/dist/health/startup-healing.test.js +298 -0
- package/dist/identity.js +36 -0
- package/dist/index.js +1378 -0
- package/dist/logging/logger-like.js +1 -0
- package/dist/observability/memory-sampler.js +51 -0
- package/dist/observability/memory-sampler.test.js +93 -0
- package/dist/observability/metrics.js +88 -0
- package/dist/observability/metrics.test.js +42 -0
- package/dist/onboarding/onboarding-flow.js +246 -0
- package/dist/onboarding/onboarding-flow.test.js +238 -0
- package/dist/onboarding/onboarding-writer.js +102 -0
- package/dist/onboarding/onboarding-writer.test.js +143 -0
- package/dist/pidlock.js +187 -0
- package/dist/pidlock.test.js +128 -0
- package/dist/pipeline/engine.js +206 -0
- package/dist/pipeline/engine.test.js +771 -0
- package/dist/root-policy.js +21 -0
- package/dist/root-policy.test.js +55 -0
- package/dist/runtime/claude-code-cli.js +35 -0
- package/dist/runtime/claude-code-cli.test.js +1199 -0
- package/dist/runtime/cli-adapter.js +584 -0
- package/dist/runtime/cli-output-parsers.js +108 -0
- package/dist/runtime/cli-shared.js +96 -0
- package/dist/runtime/cli-shared.test.js +104 -0
- package/dist/runtime/cli-strategy.js +6 -0
- package/dist/runtime/codex-cli.js +16 -0
- package/dist/runtime/codex-cli.test.js +862 -0
- package/dist/runtime/concurrency-limit.js +80 -0
- package/dist/runtime/concurrency-limit.test.js +137 -0
- package/dist/runtime/gemini-cli.js +16 -0
- package/dist/runtime/gemini-cli.test.js +413 -0
- package/dist/runtime/long-running-process.js +415 -0
- package/dist/runtime/long-running-process.test.js +318 -0
- package/dist/runtime/model-smoke-helpers.js +160 -0
- package/dist/runtime/model-smoke.test.js +194 -0
- package/dist/runtime/model-tiers.js +33 -0
- package/dist/runtime/model-tiers.test.js +65 -0
- package/dist/runtime/openai-auth.js +151 -0
- package/dist/runtime/openai-auth.test.js +361 -0
- package/dist/runtime/openai-compat.js +178 -0
- package/dist/runtime/openai-compat.test.js +449 -0
- package/dist/runtime/process-pool.js +93 -0
- package/dist/runtime/process-pool.test.js +148 -0
- package/dist/runtime/registry.js +15 -0
- package/dist/runtime/registry.test.js +47 -0
- package/dist/runtime/session-scanner.js +186 -0
- package/dist/runtime/session-scanner.test.js +257 -0
- package/dist/runtime/strategies/claude-strategy.js +193 -0
- package/dist/runtime/strategies/codex-strategy.js +161 -0
- package/dist/runtime/strategies/gemini-strategy.js +64 -0
- package/dist/runtime/strategies/template-strategy.js +85 -0
- package/dist/runtime/tool-capabilities.js +27 -0
- package/dist/runtime/tool-capabilities.test.js +24 -0
- package/dist/runtime/tool-labels.js +48 -0
- package/dist/runtime/types.js +2 -0
- package/dist/sessionManager.js +47 -0
- package/dist/sessions.js +18 -0
- package/dist/tasks/architecture-contract.js +33 -0
- package/dist/tasks/architecture-contract.test.js +90 -0
- package/dist/tasks/auto-tag.js +50 -0
- package/dist/tasks/auto-tag.test.js +64 -0
- package/dist/tasks/bd-cli.js +164 -0
- package/dist/tasks/bd-cli.test.js +359 -0
- package/dist/tasks/bead-sync.js +1 -0
- package/dist/tasks/context-summary.js +27 -0
- package/dist/tasks/discord-sync.js +3 -0
- package/dist/tasks/discord-sync.test.js +685 -0
- package/dist/tasks/discord-types.js +4 -0
- package/dist/tasks/find-task-by-thread.test.js +36 -0
- package/dist/tasks/forum-guard.js +81 -0
- package/dist/tasks/forum-guard.test.js +192 -0
- package/dist/tasks/initialize.js +77 -0
- package/dist/tasks/initialize.test.js +263 -0
- package/dist/tasks/logger-types.js +1 -0
- package/dist/tasks/metrics-types.js +3 -0
- package/dist/tasks/migrate.js +33 -0
- package/dist/tasks/migrate.test.js +156 -0
- package/dist/tasks/path-defaults.js +67 -0
- package/dist/tasks/path-defaults.test.js +73 -0
- package/dist/tasks/runtime-types.js +1 -0
- package/dist/tasks/service.js +33 -0
- package/dist/tasks/service.test.js +51 -0
- package/dist/tasks/store.js +238 -0
- package/dist/tasks/store.test.js +417 -0
- package/dist/tasks/sync-context.js +1 -0
- package/dist/tasks/sync-contract.js +24 -0
- package/dist/tasks/sync-contract.test.js +25 -0
- package/dist/tasks/sync-coordinator-metrics.js +41 -0
- package/dist/tasks/sync-coordinator-retries.js +71 -0
- package/dist/tasks/sync-coordinator.js +96 -0
- package/dist/tasks/sync-coordinator.test.js +501 -0
- package/dist/tasks/sync-types.js +1 -0
- package/dist/tasks/sync-watcher.js +27 -0
- package/dist/tasks/sync-watcher.test.js +92 -0
- package/dist/tasks/tag-map.js +36 -0
- package/dist/tasks/tag-map.test.js +54 -0
- package/dist/tasks/task-action-contract.js +16 -0
- package/dist/tasks/task-action-contract.test.js +16 -0
- package/dist/tasks/task-action-executor.js +18 -0
- package/dist/tasks/task-action-executor.test.js +420 -0
- package/dist/tasks/task-action-mutation-helpers.js +17 -0
- package/dist/tasks/task-action-mutations.js +151 -0
- package/dist/tasks/task-action-prompt.js +62 -0
- package/dist/tasks/task-action-read-ops.js +73 -0
- package/dist/tasks/task-action-runner-types.js +1 -0
- package/dist/tasks/task-action-thread-sync.js +82 -0
- package/dist/tasks/task-actions.js +3 -0
- package/dist/tasks/task-cli.js +227 -0
- package/dist/tasks/task-context.js +1 -0
- package/dist/tasks/task-lifecycle.js +46 -0
- package/dist/tasks/task-lifecycle.test.js +35 -0
- package/dist/tasks/task-sync-apply-plan.js +95 -0
- package/dist/tasks/task-sync-apply-types.js +12 -0
- package/dist/tasks/task-sync-apply.js +319 -0
- package/dist/tasks/task-sync-cli.js +89 -0
- package/dist/tasks/task-sync-cli.test.js +70 -0
- package/dist/tasks/task-sync-engine.js +88 -0
- package/dist/tasks/task-sync-engine.test.js +934 -0
- package/dist/tasks/task-sync-phase-apply.js +171 -0
- package/dist/tasks/task-sync-pipeline.js +2 -0
- package/dist/tasks/task-sync-pipeline.test.js +265 -0
- package/dist/tasks/task-sync-reconcile-plan.js +182 -0
- package/dist/tasks/task-sync-reconcile.js +144 -0
- package/dist/tasks/task-sync.js +56 -0
- package/dist/tasks/task-sync.test.js +86 -0
- package/dist/tasks/thread-cache.js +42 -0
- package/dist/tasks/thread-cache.test.js +89 -0
- package/dist/tasks/thread-contracts.test.js +711 -0
- package/dist/tasks/thread-forum-ops.js +68 -0
- package/dist/tasks/thread-helpers.js +86 -0
- package/dist/tasks/thread-helpers.test.js +33 -0
- package/dist/tasks/thread-lifecycle-ops.js +144 -0
- package/dist/tasks/thread-ops-shared.js +21 -0
- package/dist/tasks/thread-ops.js +2 -0
- package/dist/tasks/types.js +20 -0
- package/dist/tasks/types.test.js +60 -0
- package/dist/test-setup.js +11 -0
- package/dist/test-setup.test.js +42 -0
- package/dist/transport/types.js +1 -0
- package/dist/validate.js +41 -0
- package/dist/validate.test.js +94 -0
- package/dist/version.js +15 -0
- package/dist/version.test.js +31 -0
- package/dist/webhook/server.js +199 -0
- package/dist/webhook/server.test.js +460 -0
- package/dist/workspace-bootstrap.js +135 -0
- package/dist/workspace-bootstrap.test.js +514 -0
- package/dist/workspace-permissions.js +134 -0
- package/dist/workspace-permissions.test.js +181 -0
- package/package.json +74 -0
- package/scripts/cron/cron-tag-map.json +9 -0
- package/scripts/tasks/tag-map.json +10 -0
- package/systemd/discoclaw.service +19 -0
- package/templates/recipes/integration.discoclaw-recipe.md +171 -0
- package/templates/workspace/AGENTS.md +217 -0
- package/templates/workspace/BOOTSTRAP.md +1 -0
- package/templates/workspace/HEARTBEAT.md +10 -0
- package/templates/workspace/IDENTITY.md +16 -0
- package/templates/workspace/MEMORY.md +24 -0
- package/templates/workspace/SOUL.md +52 -0
- package/templates/workspace/TOOLS.md +304 -0
- package/templates/workspace/USER.md +37 -0
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
// Universal CLI runtime adapter factory.
|
|
2
|
+
// Given a thin strategy (model-specific logic), creates a full RuntimeAdapter
|
|
3
|
+
// with all shared infrastructure: subprocess tracking, process pooling,
|
|
4
|
+
// stream stall detection, session scanning, JSONL parsing, image support, etc.
|
|
5
|
+
import { execa } from 'execa';
|
|
6
|
+
import { MAX_IMAGES_PER_INVOCATION } from './types.js';
|
|
7
|
+
import { SessionFileScanner } from './session-scanner.js';
|
|
8
|
+
import { ProcessPool } from './process-pool.js';
|
|
9
|
+
import { STDIN_THRESHOLD, tryParseJsonLine, createEventQueue, SubprocessTracker, cliExecaEnv, LineBuffer, } from './cli-shared.js';
|
|
10
|
+
import { extractTextFromUnknownEvent, extractResultText, extractImageFromUnknownEvent, extractResultContentBlocks, imageDedupeKey, stripToolUseBlocks, } from './cli-output-parsers.js';
|
|
11
|
+
// Global subprocess tracker shared across all CLI adapters.
|
|
12
|
+
const globalTracker = new SubprocessTracker();
|
|
13
|
+
/** SIGKILL all tracked CLI subprocesses across all adapters (e.g. on SIGTERM). */
|
|
14
|
+
export function killAllSubprocesses() {
|
|
15
|
+
globalTracker.killAll();
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Create a RuntimeAdapter from a strategy + universal options.
|
|
19
|
+
* The strategy provides model-specific arg building, output parsing, and error handling.
|
|
20
|
+
* The universal options control shared features (multi-turn, stall detection, etc.).
|
|
21
|
+
*/
|
|
22
|
+
export function createCliRuntime(strategy, opts) {
|
|
23
|
+
const binary = opts.binary ?? strategy.binaryDefault;
|
|
24
|
+
const capabilities = new Set(strategy.capabilities);
|
|
25
|
+
if (opts.multiTurn && strategy.multiTurnMode === 'process-pool') {
|
|
26
|
+
capabilities.add('multi_turn');
|
|
27
|
+
}
|
|
28
|
+
if (!opts.disableSessions) {
|
|
29
|
+
// Sessions are enabled by default for strategies that support them.
|
|
30
|
+
// (strategies without session support simply don't list 'sessions' in capabilities)
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
capabilities.delete('sessions');
|
|
34
|
+
}
|
|
35
|
+
// Multi-turn process pool (only for process-pool mode).
|
|
36
|
+
let pool = null;
|
|
37
|
+
if (opts.multiTurn && strategy.multiTurnMode === 'process-pool') {
|
|
38
|
+
const logForPool = opts.log && typeof opts.log.info === 'function'
|
|
39
|
+
? opts.log
|
|
40
|
+
: undefined;
|
|
41
|
+
pool = new ProcessPool({
|
|
42
|
+
maxProcesses: opts.multiTurnMaxProcesses ?? 5,
|
|
43
|
+
log: logForPool,
|
|
44
|
+
});
|
|
45
|
+
globalTracker.addPool(pool);
|
|
46
|
+
}
|
|
47
|
+
// Session resume map (for session-resume mode like Codex).
|
|
48
|
+
const sessionMap = (strategy.multiTurnMode === 'session-resume' && !opts.disableSessions)
|
|
49
|
+
? new Map()
|
|
50
|
+
: null;
|
|
51
|
+
async function* invoke(params) {
|
|
52
|
+
const model = params.model || strategy.defaultModel;
|
|
53
|
+
// ---------------------------------------------------------------
|
|
54
|
+
// Multi-turn: process pool path (Claude-style)
|
|
55
|
+
// ---------------------------------------------------------------
|
|
56
|
+
if (pool && params.sessionKey) {
|
|
57
|
+
try {
|
|
58
|
+
const proc = pool.getOrSpawn(params.sessionKey, {
|
|
59
|
+
claudeBin: binary,
|
|
60
|
+
model,
|
|
61
|
+
cwd: params.cwd,
|
|
62
|
+
dangerouslySkipPermissions: opts.dangerouslySkipPermissions,
|
|
63
|
+
strictMcpConfig: opts.strictMcpConfig,
|
|
64
|
+
fallbackModel: opts.fallbackModel,
|
|
65
|
+
maxBudgetUsd: opts.maxBudgetUsd,
|
|
66
|
+
appendSystemPrompt: opts.appendSystemPrompt,
|
|
67
|
+
verbose: opts.verbose,
|
|
68
|
+
tools: params.tools,
|
|
69
|
+
addDirs: params.addDirs,
|
|
70
|
+
hangTimeoutMs: opts.multiTurnHangTimeoutMs,
|
|
71
|
+
idleTimeoutMs: opts.multiTurnIdleTimeoutMs,
|
|
72
|
+
log: pool && opts.log && typeof opts.log.info === 'function'
|
|
73
|
+
? opts.log
|
|
74
|
+
: undefined,
|
|
75
|
+
});
|
|
76
|
+
if (proc?.isAlive) {
|
|
77
|
+
const sub = proc.getSubprocess();
|
|
78
|
+
if (sub)
|
|
79
|
+
globalTracker.add(sub);
|
|
80
|
+
// Abort the pool turn if the caller's signal fires.
|
|
81
|
+
if (params.signal?.aborted) {
|
|
82
|
+
pool.remove(params.sessionKey);
|
|
83
|
+
if (sub)
|
|
84
|
+
globalTracker.delete(sub);
|
|
85
|
+
yield { type: 'error', message: 'aborted' };
|
|
86
|
+
yield { type: 'done' };
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const onPoolAbort = () => { proc.kill?.(); };
|
|
90
|
+
params.signal?.addEventListener('abort', onPoolAbort, { once: true });
|
|
91
|
+
let fallback = false;
|
|
92
|
+
try {
|
|
93
|
+
for await (const evt of proc.sendTurn(params.prompt, params.images)) {
|
|
94
|
+
if (evt.type === 'error' && (evt.message.startsWith('long-running:') || evt.message.includes('hang detected'))) {
|
|
95
|
+
pool.remove(params.sessionKey);
|
|
96
|
+
fallback = true;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
yield evt;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
params.signal?.removeEventListener('abort', onPoolAbort);
|
|
104
|
+
}
|
|
105
|
+
if (sub)
|
|
106
|
+
globalTracker.delete(sub);
|
|
107
|
+
if (!fallback)
|
|
108
|
+
return;
|
|
109
|
+
opts.log?.info?.('multi-turn: process failed, falling back to one-shot');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
opts.log?.info?.({ err }, 'multi-turn: error, falling back to one-shot');
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// ---------------------------------------------------------------
|
|
117
|
+
// One-shot path
|
|
118
|
+
// ---------------------------------------------------------------
|
|
119
|
+
if (params.signal?.aborted) {
|
|
120
|
+
yield { type: 'error', message: 'aborted' };
|
|
121
|
+
yield { type: 'done' };
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const hasImages = Boolean(params.images && params.images.length > 0);
|
|
125
|
+
const promptTooLarge = Buffer.byteLength(params.prompt, 'utf-8') > STDIN_THRESHOLD;
|
|
126
|
+
const useStdin = hasImages || promptTooLarge;
|
|
127
|
+
const ctx = { params: { ...params, model }, useStdin, hasImages, sessionMap: sessionMap ?? undefined };
|
|
128
|
+
const args = strategy.buildArgs(ctx, opts);
|
|
129
|
+
const outputMode = strategy.getOutputMode(ctx, opts);
|
|
130
|
+
if (opts.log) {
|
|
131
|
+
opts.log.debug({ args: args.slice(0, -1), hasImages, promptTooLarge, useStdin }, `${strategy.id}: constructed args`);
|
|
132
|
+
}
|
|
133
|
+
const subprocess = execa(binary, args, {
|
|
134
|
+
cwd: params.cwd,
|
|
135
|
+
timeout: params.timeoutMs,
|
|
136
|
+
reject: false,
|
|
137
|
+
forceKillAfterDelay: 5000,
|
|
138
|
+
stdin: useStdin ? 'pipe' : 'ignore',
|
|
139
|
+
env: cliExecaEnv(),
|
|
140
|
+
stdout: 'pipe',
|
|
141
|
+
stderr: 'pipe',
|
|
142
|
+
});
|
|
143
|
+
// Write stdin payload if needed.
|
|
144
|
+
if (useStdin && subprocess.stdin) {
|
|
145
|
+
const payload = strategy.buildStdinPayload?.(ctx);
|
|
146
|
+
if (payload) {
|
|
147
|
+
try {
|
|
148
|
+
subprocess.stdin.write(payload);
|
|
149
|
+
subprocess.stdin.end();
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// stdin write failed — process will exit with error.
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
globalTracker.add(subprocess);
|
|
157
|
+
subprocess.then(() => globalTracker.delete(subprocess))
|
|
158
|
+
.catch(() => globalTracker.delete(subprocess));
|
|
159
|
+
// Wire caller's AbortSignal to kill the subprocess.
|
|
160
|
+
const onAbort = () => {
|
|
161
|
+
subprocess.kill('SIGKILL');
|
|
162
|
+
if (!finished) {
|
|
163
|
+
push({ type: 'error', message: 'aborted' });
|
|
164
|
+
push({ type: 'done' });
|
|
165
|
+
finished = true;
|
|
166
|
+
wake();
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
params.signal?.addEventListener('abort', onAbort, { once: true });
|
|
170
|
+
if (!subprocess.stdout) {
|
|
171
|
+
yield { type: 'error', message: `${strategy.id}: missing stdout stream` };
|
|
172
|
+
yield { type: 'done' };
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const { q, push, wait, wake } = createEventQueue();
|
|
176
|
+
// --- Stream stall detection ---
|
|
177
|
+
let finished = false;
|
|
178
|
+
let stallTimer = null;
|
|
179
|
+
const clearStallTimer = () => { if (stallTimer) {
|
|
180
|
+
clearTimeout(stallTimer);
|
|
181
|
+
stallTimer = null;
|
|
182
|
+
} };
|
|
183
|
+
const resetStallTimer = () => {
|
|
184
|
+
if (!opts.streamStallTimeoutMs)
|
|
185
|
+
return;
|
|
186
|
+
clearStallTimer();
|
|
187
|
+
stallTimer = setTimeout(() => {
|
|
188
|
+
const ms = opts.streamStallTimeoutMs;
|
|
189
|
+
opts.log?.info?.(`one-shot: stream stall detected, killing process`);
|
|
190
|
+
push({ type: 'error', message: `stream stall: no output for ${ms}ms` });
|
|
191
|
+
push({ type: 'done' });
|
|
192
|
+
finished = true;
|
|
193
|
+
subprocess.kill('SIGTERM');
|
|
194
|
+
wake();
|
|
195
|
+
}, opts.streamStallTimeoutMs);
|
|
196
|
+
};
|
|
197
|
+
resetStallTimer();
|
|
198
|
+
// --- Progress stall detection (thinking spiral guard) ---
|
|
199
|
+
// Unlike stallTimer which resets on any raw data chunk, progressTimer
|
|
200
|
+
// resets only when a text_delta event is pushed — catching scenarios
|
|
201
|
+
// where thinking tokens flow through stdout but no useful content appears.
|
|
202
|
+
let progressTimer = null;
|
|
203
|
+
let progressResetCount = 0;
|
|
204
|
+
const clearProgressTimer = () => { if (progressTimer) {
|
|
205
|
+
clearTimeout(progressTimer);
|
|
206
|
+
progressTimer = null;
|
|
207
|
+
} };
|
|
208
|
+
const resetProgressTimer = () => {
|
|
209
|
+
if (!opts.progressStallTimeoutMs)
|
|
210
|
+
return;
|
|
211
|
+
clearProgressTimer();
|
|
212
|
+
progressResetCount++;
|
|
213
|
+
if (progressResetCount === 1) {
|
|
214
|
+
opts.log?.debug?.(`progress-timer: armed (${opts.progressStallTimeoutMs}ms)`);
|
|
215
|
+
}
|
|
216
|
+
progressTimer = setTimeout(() => {
|
|
217
|
+
if (finished)
|
|
218
|
+
return;
|
|
219
|
+
const ms = opts.progressStallTimeoutMs;
|
|
220
|
+
opts.log?.info?.(`one-shot: progress stall detected (no text_delta for ${ms}ms, resets=${progressResetCount}), killing process`);
|
|
221
|
+
push({ type: 'error', message: `progress stall: no text output for ${ms}ms (possible thinking spiral)` });
|
|
222
|
+
push({ type: 'done' });
|
|
223
|
+
finished = true;
|
|
224
|
+
subprocess.kill('SIGTERM');
|
|
225
|
+
wake();
|
|
226
|
+
}, opts.progressStallTimeoutMs);
|
|
227
|
+
};
|
|
228
|
+
resetProgressTimer();
|
|
229
|
+
// --- Session file scanner (Claude-specific, but controlled by opts) ---
|
|
230
|
+
let scanner = null;
|
|
231
|
+
if (opts.sessionScanning && params.sessionId) {
|
|
232
|
+
scanner = new SessionFileScanner({ sessionId: params.sessionId, cwd: params.cwd, log: opts.log }, { onEvent: push });
|
|
233
|
+
scanner.start().catch((err) => opts.log?.debug({ err }, 'session-scanner: start failed'));
|
|
234
|
+
}
|
|
235
|
+
// --- State variables ---
|
|
236
|
+
let mergedStdout = '';
|
|
237
|
+
let merged = '';
|
|
238
|
+
let resultText = '';
|
|
239
|
+
let inToolUse = false;
|
|
240
|
+
const stdoutLineBuf = new LineBuffer();
|
|
241
|
+
let stderrBuffered = '';
|
|
242
|
+
let stderrForError = '';
|
|
243
|
+
let stdoutEnded = false;
|
|
244
|
+
let stderrEnded = subprocess.stderr == null;
|
|
245
|
+
let procResult = null;
|
|
246
|
+
const seenImages = new Set();
|
|
247
|
+
let imageCount = 0;
|
|
248
|
+
// --- Stdout handler ---
|
|
249
|
+
subprocess.stdout.on('data', (chunk) => {
|
|
250
|
+
resetStallTimer();
|
|
251
|
+
const s = String(chunk);
|
|
252
|
+
mergedStdout += s;
|
|
253
|
+
if (outputMode === 'text') {
|
|
254
|
+
push({ type: 'text_delta', text: s });
|
|
255
|
+
resetProgressTimer();
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
// JSONL mode: parse line-delimited events.
|
|
259
|
+
const lines = stdoutLineBuf.feed(s);
|
|
260
|
+
for (const line of lines) {
|
|
261
|
+
const trimmed = line.trim();
|
|
262
|
+
if (!trimmed)
|
|
263
|
+
continue;
|
|
264
|
+
if (opts.echoStdio) {
|
|
265
|
+
push({ type: 'log_line', stream: 'stdout', line: trimmed });
|
|
266
|
+
}
|
|
267
|
+
const evt = tryParseJsonLine(trimmed);
|
|
268
|
+
// Delegate to strategy parser first.
|
|
269
|
+
if (strategy.parseLine && evt) {
|
|
270
|
+
const parsed = strategy.parseLine(evt, ctx);
|
|
271
|
+
if (parsed) {
|
|
272
|
+
// Emit extra events (e.g. session mapping).
|
|
273
|
+
if (parsed.extraEvents) {
|
|
274
|
+
for (const e of parsed.extraEvents)
|
|
275
|
+
push(e);
|
|
276
|
+
}
|
|
277
|
+
// Handle text.
|
|
278
|
+
if (parsed.text) {
|
|
279
|
+
merged += parsed.text;
|
|
280
|
+
if (parsed.inToolUse !== undefined) {
|
|
281
|
+
if (parsed.inToolUse)
|
|
282
|
+
inToolUse = true;
|
|
283
|
+
}
|
|
284
|
+
if (!inToolUse) {
|
|
285
|
+
push({ type: 'text_delta', text: parsed.text });
|
|
286
|
+
resetProgressTimer();
|
|
287
|
+
}
|
|
288
|
+
if (parsed.inToolUse === false)
|
|
289
|
+
inToolUse = false;
|
|
290
|
+
}
|
|
291
|
+
else if (parsed.activity) {
|
|
292
|
+
// Non-text activity (e.g. tool input generation) — reset the
|
|
293
|
+
// progress stall timer so legitimate work isn't killed.
|
|
294
|
+
resetProgressTimer();
|
|
295
|
+
}
|
|
296
|
+
// Handle result text.
|
|
297
|
+
if (parsed.resultText)
|
|
298
|
+
resultText = parsed.resultText;
|
|
299
|
+
// Handle images.
|
|
300
|
+
if (parsed.image && imageCount < MAX_IMAGES_PER_INVOCATION) {
|
|
301
|
+
const key = imageDedupeKey(parsed.image);
|
|
302
|
+
if (!seenImages.has(key)) {
|
|
303
|
+
seenImages.add(key);
|
|
304
|
+
imageCount++;
|
|
305
|
+
push({ type: 'image_data', image: parsed.image });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (parsed.resultImages) {
|
|
309
|
+
for (const img of parsed.resultImages) {
|
|
310
|
+
if (imageCount >= MAX_IMAGES_PER_INVOCATION)
|
|
311
|
+
break;
|
|
312
|
+
const key = imageDedupeKey(img);
|
|
313
|
+
if (!seenImages.has(key)) {
|
|
314
|
+
seenImages.add(key);
|
|
315
|
+
imageCount++;
|
|
316
|
+
push({ type: 'image_data', image: img });
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
continue; // Strategy handled this line.
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// Default JSONL parsing (Claude-compatible fallback).
|
|
324
|
+
const text = extractTextFromUnknownEvent(evt ?? trimmed);
|
|
325
|
+
if (text) {
|
|
326
|
+
merged += text;
|
|
327
|
+
const hasToolOpen = text.includes('<tool_use>') || text.includes('<tool_calls>') || text.includes('<tool_call>') || text.includes('<tool_results>') || text.includes('<tool_result>');
|
|
328
|
+
const hasToolClose = text.includes('</tool_use>') || text.includes('</tool_calls>') || text.includes('</tool_call>') || text.includes('</tool_results>') || text.includes('</tool_result>');
|
|
329
|
+
if (hasToolOpen)
|
|
330
|
+
inToolUse = true;
|
|
331
|
+
if (!inToolUse) {
|
|
332
|
+
push({ type: 'text_delta', text });
|
|
333
|
+
resetProgressTimer();
|
|
334
|
+
}
|
|
335
|
+
if (hasToolClose)
|
|
336
|
+
inToolUse = false;
|
|
337
|
+
}
|
|
338
|
+
else if (evt) {
|
|
339
|
+
const rt = extractResultText(evt);
|
|
340
|
+
if (rt)
|
|
341
|
+
resultText = rt;
|
|
342
|
+
const blocks = extractResultContentBlocks(evt);
|
|
343
|
+
if (blocks) {
|
|
344
|
+
if (blocks.text)
|
|
345
|
+
resultText = blocks.text;
|
|
346
|
+
for (const img of blocks.images) {
|
|
347
|
+
if (imageCount >= MAX_IMAGES_PER_INVOCATION)
|
|
348
|
+
break;
|
|
349
|
+
const key = imageDedupeKey(img);
|
|
350
|
+
if (!seenImages.has(key)) {
|
|
351
|
+
seenImages.add(key);
|
|
352
|
+
imageCount++;
|
|
353
|
+
push({ type: 'image_data', image: img });
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const img = extractImageFromUnknownEvent(evt);
|
|
358
|
+
if (img && imageCount < MAX_IMAGES_PER_INVOCATION) {
|
|
359
|
+
const key = imageDedupeKey(img);
|
|
360
|
+
if (!seenImages.has(key)) {
|
|
361
|
+
seenImages.add(key);
|
|
362
|
+
imageCount++;
|
|
363
|
+
push({ type: 'image_data', image: img });
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
// --- Stderr handler ---
|
|
370
|
+
subprocess.stderr?.on('data', (chunk) => {
|
|
371
|
+
resetStallTimer();
|
|
372
|
+
const s = String(chunk);
|
|
373
|
+
stderrForError += s;
|
|
374
|
+
if (!opts.echoStdio)
|
|
375
|
+
return;
|
|
376
|
+
stderrBuffered += s;
|
|
377
|
+
const lines = stderrBuffered.split(/\r?\n/);
|
|
378
|
+
stderrBuffered = lines.pop() ?? '';
|
|
379
|
+
for (const line of lines) {
|
|
380
|
+
push({ type: 'log_line', stream: 'stderr', line });
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
// --- Stream end handlers ---
|
|
384
|
+
subprocess.stdout.on('end', () => {
|
|
385
|
+
stdoutEnded = true;
|
|
386
|
+
tryFinalize();
|
|
387
|
+
});
|
|
388
|
+
subprocess.stderr?.on('end', () => {
|
|
389
|
+
stderrEnded = true;
|
|
390
|
+
tryFinalize();
|
|
391
|
+
});
|
|
392
|
+
function tryFinalize() {
|
|
393
|
+
if (finished)
|
|
394
|
+
return;
|
|
395
|
+
if (!procResult)
|
|
396
|
+
return;
|
|
397
|
+
if (!stdoutEnded)
|
|
398
|
+
return;
|
|
399
|
+
if (!stderrEnded)
|
|
400
|
+
return;
|
|
401
|
+
clearStallTimer();
|
|
402
|
+
clearProgressTimer();
|
|
403
|
+
const exitCode = procResult.exitCode;
|
|
404
|
+
const stdout = procResult.stdout ?? '';
|
|
405
|
+
const stderr = procResult.stderr ?? '';
|
|
406
|
+
if (params.signal?.aborted) {
|
|
407
|
+
push({ type: 'error', message: 'aborted' });
|
|
408
|
+
push({ type: 'done' });
|
|
409
|
+
finished = true;
|
|
410
|
+
wake();
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (procResult.timedOut) {
|
|
414
|
+
// Use a fixed message — execa's originalMessage/shortMessage can contain the
|
|
415
|
+
// full command line (including prompt text), so we never expose raw error strings.
|
|
416
|
+
push({
|
|
417
|
+
type: 'error',
|
|
418
|
+
message: `${strategy.id === 'claude_code' ? 'claude' : strategy.id} timed out after ${params.timeoutMs ?? 0}ms`,
|
|
419
|
+
});
|
|
420
|
+
push({ type: 'done' });
|
|
421
|
+
finished = true;
|
|
422
|
+
wake();
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
// Spawn failure (no exit code).
|
|
426
|
+
if (procResult.failed && exitCode == null) {
|
|
427
|
+
const spawnMsg = strategy.handleSpawnError?.(procResult, binary);
|
|
428
|
+
if (spawnMsg) {
|
|
429
|
+
push({ type: 'error', message: spawnMsg });
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
const msg = (procResult.shortMessage || procResult.originalMessage || procResult.message || '').trim();
|
|
433
|
+
push({ type: 'error', message: msg || `${strategy.id} failed (no exit code)` });
|
|
434
|
+
}
|
|
435
|
+
push({ type: 'done' });
|
|
436
|
+
finished = true;
|
|
437
|
+
wake();
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
// Flush trailing stderr.
|
|
441
|
+
const stderrTail = stderrBuffered.trimEnd();
|
|
442
|
+
if (opts.echoStdio && stderrTail) {
|
|
443
|
+
push({ type: 'log_line', stream: 'stderr', line: stderrTail });
|
|
444
|
+
}
|
|
445
|
+
// Flush trailing stdout buffer.
|
|
446
|
+
if (outputMode === 'jsonl') {
|
|
447
|
+
const tail = stdoutLineBuf.flush().trim();
|
|
448
|
+
if (tail) {
|
|
449
|
+
const evt = tryParseJsonLine(tail);
|
|
450
|
+
// Let strategy parse trailing line.
|
|
451
|
+
if (strategy.parseLine && evt) {
|
|
452
|
+
const parsed = strategy.parseLine(evt, ctx);
|
|
453
|
+
if (parsed) {
|
|
454
|
+
if (parsed.text) {
|
|
455
|
+
merged += parsed.text;
|
|
456
|
+
push({ type: 'text_delta', text: parsed.text });
|
|
457
|
+
}
|
|
458
|
+
if (parsed.resultText)
|
|
459
|
+
resultText = parsed.resultText;
|
|
460
|
+
if (parsed.image && imageCount < MAX_IMAGES_PER_INVOCATION) {
|
|
461
|
+
const key = imageDedupeKey(parsed.image);
|
|
462
|
+
if (!seenImages.has(key)) {
|
|
463
|
+
seenImages.add(key);
|
|
464
|
+
imageCount++;
|
|
465
|
+
push({ type: 'image_data', image: parsed.image });
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
// Default trailing buffer parsing.
|
|
472
|
+
const text = extractTextFromUnknownEvent(evt ?? tail);
|
|
473
|
+
if (text) {
|
|
474
|
+
merged += text;
|
|
475
|
+
push({ type: 'text_delta', text });
|
|
476
|
+
}
|
|
477
|
+
if (evt) {
|
|
478
|
+
const img = extractImageFromUnknownEvent(evt);
|
|
479
|
+
if (img && imageCount < MAX_IMAGES_PER_INVOCATION) {
|
|
480
|
+
const key = imageDedupeKey(img);
|
|
481
|
+
if (!seenImages.has(key)) {
|
|
482
|
+
seenImages.add(key);
|
|
483
|
+
imageCount++;
|
|
484
|
+
push({ type: 'image_data', image: img });
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
// Non-zero exit.
|
|
492
|
+
if (exitCode !== 0) {
|
|
493
|
+
// Clear stale session on error (session-resume mode).
|
|
494
|
+
if (sessionMap && params.sessionKey)
|
|
495
|
+
sessionMap.delete(params.sessionKey);
|
|
496
|
+
const exitMsg = strategy.handleExitError?.(exitCode, stderrForError || stderr, stdout);
|
|
497
|
+
if (exitMsg) {
|
|
498
|
+
push({ type: 'error', message: exitMsg });
|
|
499
|
+
}
|
|
500
|
+
else {
|
|
501
|
+
const raw = (stderrForError || stderr || stdout || `${strategy.id} exit ${exitCode}`).trim();
|
|
502
|
+
const sanitized = strategy.sanitizeError?.(raw, binary) ?? raw;
|
|
503
|
+
push({ type: 'error', message: sanitized });
|
|
504
|
+
}
|
|
505
|
+
push({ type: 'done' });
|
|
506
|
+
finished = true;
|
|
507
|
+
wake();
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
// Success.
|
|
511
|
+
if (outputMode === 'text') {
|
|
512
|
+
const final = (stdout || mergedStdout).trimEnd();
|
|
513
|
+
if (final)
|
|
514
|
+
push({ type: 'text_final', text: final });
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
const raw = resultText.trim() || (merged.trim() ? merged.trimEnd() : '');
|
|
518
|
+
const final = stripToolUseBlocks(raw);
|
|
519
|
+
if (final)
|
|
520
|
+
push({ type: 'text_final', text: final });
|
|
521
|
+
}
|
|
522
|
+
push({ type: 'done' });
|
|
523
|
+
finished = true;
|
|
524
|
+
wake();
|
|
525
|
+
}
|
|
526
|
+
// --- Process completion ---
|
|
527
|
+
subprocess.then((result) => {
|
|
528
|
+
procResult = result;
|
|
529
|
+
tryFinalize();
|
|
530
|
+
}).catch((err) => {
|
|
531
|
+
clearStallTimer();
|
|
532
|
+
if (finished)
|
|
533
|
+
return;
|
|
534
|
+
// Check timeout first — use fixed message to avoid leaking prompt/command line.
|
|
535
|
+
if (err?.timedOut) {
|
|
536
|
+
push({
|
|
537
|
+
type: 'error',
|
|
538
|
+
message: `${strategy.id === 'claude_code' ? 'claude' : strategy.id} timed out after ${params.timeoutMs ?? 0}ms`,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
const spawnMsg = strategy.handleSpawnError?.(err, binary);
|
|
543
|
+
if (spawnMsg) {
|
|
544
|
+
push({ type: 'error', message: spawnMsg });
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
const msg = String((err?.originalMessage || err?.shortMessage || err?.message || err || '')).trim();
|
|
548
|
+
push({
|
|
549
|
+
type: 'error',
|
|
550
|
+
message: msg || `${strategy.id} failed`,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
push({ type: 'done' });
|
|
555
|
+
finished = true;
|
|
556
|
+
wake();
|
|
557
|
+
});
|
|
558
|
+
// --- Yield events ---
|
|
559
|
+
try {
|
|
560
|
+
while (!finished || q.length > 0) {
|
|
561
|
+
if (q.length === 0)
|
|
562
|
+
await wait();
|
|
563
|
+
while (q.length > 0) {
|
|
564
|
+
yield q.shift();
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
finally {
|
|
569
|
+
clearStallTimer();
|
|
570
|
+
clearProgressTimer();
|
|
571
|
+
scanner?.stop();
|
|
572
|
+
params.signal?.removeEventListener('abort', onAbort);
|
|
573
|
+
if (!finished)
|
|
574
|
+
subprocess.kill('SIGKILL');
|
|
575
|
+
globalTracker.delete(subprocess);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return {
|
|
579
|
+
id: strategy.id,
|
|
580
|
+
capabilities,
|
|
581
|
+
defaultModel: strategy.defaultModel,
|
|
582
|
+
invoke,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Output parsing functions for Claude CLI stream-json format.
|
|
2
|
+
// Extracted from claude-code-cli.ts for reuse by strategies and LongRunningProcess
|
|
3
|
+
// without circular imports.
|
|
4
|
+
/** Max base64 string length (~25 MB encoded, ~18.75 MB decoded). */
|
|
5
|
+
const MAX_IMAGE_BASE64_LEN = 25 * 1024 * 1024;
|
|
6
|
+
/** Extract a text string from a Claude CLI stream-json event. */
|
|
7
|
+
export function extractTextFromUnknownEvent(evt) {
|
|
8
|
+
if (!evt || typeof evt !== 'object')
|
|
9
|
+
return null;
|
|
10
|
+
const anyEvt = evt;
|
|
11
|
+
// Claude CLI stream-json emits nested structures; check common shapes.
|
|
12
|
+
const candidates = [
|
|
13
|
+
anyEvt.text,
|
|
14
|
+
anyEvt.delta,
|
|
15
|
+
anyEvt.content,
|
|
16
|
+
// Sometimes nested under .data.
|
|
17
|
+
(anyEvt.data && typeof anyEvt.data === 'object') ? anyEvt.data.text : undefined,
|
|
18
|
+
// Claude CLI stream-json: event.delta.text (content_block_delta events)
|
|
19
|
+
(anyEvt.event && typeof anyEvt.event === 'object' &&
|
|
20
|
+
anyEvt.event.delta && typeof anyEvt.event.delta === 'object')
|
|
21
|
+
? anyEvt.event.delta.text
|
|
22
|
+
: undefined,
|
|
23
|
+
];
|
|
24
|
+
for (const c of candidates) {
|
|
25
|
+
if (typeof c === 'string' && c.length > 0)
|
|
26
|
+
return c;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
/** Extract the final result text from a Claude CLI stream-json "result" event. */
|
|
31
|
+
export function extractResultText(evt) {
|
|
32
|
+
if (!evt || typeof evt !== 'object')
|
|
33
|
+
return null;
|
|
34
|
+
const anyEvt = evt;
|
|
35
|
+
if (anyEvt.type === 'result' && typeof anyEvt.result === 'string' && anyEvt.result.length > 0) {
|
|
36
|
+
return anyEvt.result;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
/** Extract an image content block from a Claude CLI stream-json event. */
|
|
41
|
+
export function extractImageFromUnknownEvent(evt) {
|
|
42
|
+
if (!evt || typeof evt !== 'object')
|
|
43
|
+
return null;
|
|
44
|
+
const anyEvt = evt;
|
|
45
|
+
// Direct image content block: { type: 'image', source: { type: 'base64', media_type, data } }
|
|
46
|
+
if (anyEvt.type === 'image' && anyEvt.source && typeof anyEvt.source === 'object') {
|
|
47
|
+
const src = anyEvt.source;
|
|
48
|
+
if (src.type === 'base64' && typeof src.media_type === 'string' && typeof src.data === 'string') {
|
|
49
|
+
if (src.data.length > MAX_IMAGE_BASE64_LEN)
|
|
50
|
+
return null;
|
|
51
|
+
return { base64: src.data, mediaType: src.media_type };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Wrapped in content_block_start: { content_block: { type: 'image', source: { ... } } }
|
|
55
|
+
if (anyEvt.content_block && typeof anyEvt.content_block === 'object') {
|
|
56
|
+
return extractImageFromUnknownEvent(anyEvt.content_block);
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
/** Extract text and images from a result event with content block arrays. */
|
|
61
|
+
export function extractResultContentBlocks(evt) {
|
|
62
|
+
if (!evt || typeof evt !== 'object')
|
|
63
|
+
return null;
|
|
64
|
+
const anyEvt = evt;
|
|
65
|
+
if (anyEvt.type !== 'result' || !Array.isArray(anyEvt.result))
|
|
66
|
+
return null;
|
|
67
|
+
let text = '';
|
|
68
|
+
const images = [];
|
|
69
|
+
for (const block of anyEvt.result) {
|
|
70
|
+
if (!block || typeof block !== 'object')
|
|
71
|
+
continue;
|
|
72
|
+
const b = block;
|
|
73
|
+
if (b.type === 'text' && typeof b.text === 'string') {
|
|
74
|
+
text += b.text;
|
|
75
|
+
}
|
|
76
|
+
else if (b.type === 'image') {
|
|
77
|
+
const img = extractImageFromUnknownEvent(b);
|
|
78
|
+
if (img)
|
|
79
|
+
images.push(img);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { text, images };
|
|
83
|
+
}
|
|
84
|
+
/** Create a dedupe key for an image using a prefix + length to avoid storing full base64 in memory. */
|
|
85
|
+
export function imageDedupeKey(img) {
|
|
86
|
+
return img.mediaType + ':' + img.base64.length + ':' + img.base64.slice(0, 64);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Strip tool-call XML blocks and keep only the final answer.
|
|
90
|
+
* When tool blocks are present, the text before/between them is narration
|
|
91
|
+
* ("Let me read the files...") — we only want the text *after* the last block.
|
|
92
|
+
*/
|
|
93
|
+
export function stripToolUseBlocks(text) {
|
|
94
|
+
const toolPattern = /<tool_use>[\s\S]*?<\/tool_use>|<tool_calls>[\s\S]*?<\/tool_calls>|<tool_results>[\s\S]*?<\/tool_results>|<tool_call>[\s\S]*?<\/tool_call>|<tool_result>[\s\S]*?<\/tool_result>/g;
|
|
95
|
+
const segments = text.split(toolPattern);
|
|
96
|
+
// If tool blocks exist, keep only the last segment (the final answer).
|
|
97
|
+
const result = segments.length > 1
|
|
98
|
+
? segments[segments.length - 1] ?? ''
|
|
99
|
+
: text;
|
|
100
|
+
return result.replace(/\n{3,}/g, '\n\n').trim();
|
|
101
|
+
}
|
|
102
|
+
/** Yield a text_final + done event pair from a text string. */
|
|
103
|
+
export function* textAsChunks(text) {
|
|
104
|
+
if (!text)
|
|
105
|
+
return;
|
|
106
|
+
yield { type: 'text_final', text };
|
|
107
|
+
yield { type: 'done' };
|
|
108
|
+
}
|