mixdog 1.3.0 → 1.3.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/NOTICE.md +52 -9
- package/README.md +8 -4
- package/package.json +2 -2
- package/scripts/native-release-status.mjs +238 -0
- package/src/defaults/shipped-history.json +8 -1
- package/src/defaults/skills/pptx/scripts/qc-pages.mjs +3 -2
- package/src/defaults/skills/setup/SKILL.md +23 -1
- package/src/defaults/skills/setup/references/actions.md +54 -4
- package/src/defaults/skills/setup/references/surfaces.md +23 -3
- package/src/defaults/skills/setup-guide/SKILL.md +17 -10
- package/src/defaults/skills/visualize/SKILL.md +9 -6
- package/src/defaults/skills/visualize/references/craft.md +8 -1
- package/src/help.mjs +1 -1
- package/src/lib/keychain-cjs.cjs +4 -1
- package/src/lib/rules-builder.cjs +14 -7
- package/src/output-styles/common.md +13 -8
- package/src/rules/agent-mode/AGENT_MODE.md +9 -6
- package/src/rules/agent-mode/MIXY.md +11 -6
- package/src/rules/lead/LEAD.md +2 -2
- package/src/rules/shared/10-tool-workflow.md +4 -0
- package/src/rules/shared/20-research.md +2 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +5 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/session-prep.mjs +8 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +6 -1
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +114 -26
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +8 -0
- package/src/runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs +5 -4
- package/src/runtime/agent/orchestrator/agent-trace.mjs +3 -0
- package/src/runtime/agent/orchestrator/cache-break-trace.mjs +4 -0
- package/src/runtime/agent/orchestrator/config-presets.mjs +2 -0
- package/src/runtime/agent/orchestrator/config-storage.mjs +10 -3
- package/src/runtime/agent/orchestrator/config.mjs +11 -0
- package/src/runtime/agent/orchestrator/context/role-instructions.mjs +81 -16
- package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +97 -4
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +6 -10
- package/src/runtime/agent/orchestrator/mcp/client.mjs +7 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +80 -31
- package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +67 -38
- package/src/runtime/agent/orchestrator/providers/anthropic-turn-result.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +7 -2
- package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +6 -2
- package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +34 -16
- package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +31 -4
- package/src/runtime/agent/orchestrator/providers/cursor.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +11 -8
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-prefix-trace.mjs +76 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +19 -1
- package/src/runtime/agent/orchestrator/providers/lib/tail-reminder-split.mjs +13 -0
- package/src/runtime/agent/orchestrator/providers/lib/tool-call-wire-ids.mjs +65 -0
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +16 -1
- package/src/runtime/agent/orchestrator/providers/mixdog-local.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +5 -5
- package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +9 -5
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +9 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +8 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +10 -2
- package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +30 -3
- package/src/runtime/agent/orchestrator/providers/openai-model-capabilities.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/opencode-go-oauth.mjs +61 -8
- package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +14 -2
- package/src/runtime/agent/orchestrator/providers/registry.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +17 -0
- package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +90 -24
- package/src/runtime/agent/orchestrator/runtime-core/fs-utils.mjs +8 -1
- package/src/runtime/agent/orchestrator/runtime-core/goal-mutations.mjs +1 -1
- package/src/runtime/agent/orchestrator/runtime-core/goal-storage.mjs +68 -22
- package/src/runtime/agent/orchestrator/runtime-core/goal-tool-exec.mjs +1 -1
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +5 -5
- package/src/runtime/agent/orchestrator/runtime-core/route-context-state.mjs +81 -0
- package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +2 -1
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +12 -1
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +17 -6
- package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +41 -10
- package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +6 -4
- package/src/runtime/agent/orchestrator/runtime-core/workflow/packs.mjs +8 -4
- package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/agent-mode.mjs +0 -6
- package/src/runtime/agent/orchestrator/session/agent-room-supervisor.d.mts +1 -1
- package/src/runtime/agent/orchestrator/session/agent-room-supervisor.mjs +22 -4
- package/src/runtime/agent/orchestrator/session/agent-room.d.mts +21 -2
- package/src/runtime/agent/orchestrator/session/agent-room.mjs +149 -45
- package/src/runtime/agent/orchestrator/session/attachment-preflight.mjs +80 -0
- package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +14 -11
- package/src/runtime/agent/orchestrator/session/compact/forked-summary.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +68 -18
- package/src/runtime/agent/orchestrator/session/compact/summary-mode.mjs +16 -0
- package/src/runtime/agent/orchestrator/session/compact/summary-route.mjs +87 -0
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +41 -10
- package/src/runtime/agent/orchestrator/session/human-gate/delegation.mjs +230 -0
- package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +156 -25
- package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +20 -0
- package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +21 -9
- package/src/runtime/agent/orchestrator/session/human-gate/run-policy.mjs +73 -0
- package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +55 -27
- package/src/runtime/agent/orchestrator/session/human-gate/unattended-port.mjs +15 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +189 -105
- package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +6 -2
- package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +111 -9
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +11 -14
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +38 -5
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/dispatch.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +12 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +11 -5
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +46 -4
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +13 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +18 -8
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +44 -18
- package/src/runtime/agent/orchestrator/session/manager/orchestration-mode-switch-notice.mjs +99 -0
- package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +42 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +3 -9
- package/src/runtime/agent/orchestrator/session/manager/pending-spool-file.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/pending-spool-shard.mjs +5 -3
- package/src/runtime/agent/orchestrator/session/manager/provider-replay.mjs +50 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +40 -1
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +12 -0
- package/src/runtime/agent/orchestrator/session/manager/runtime-state.mjs +20 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +62 -47
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +6 -8
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +12 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +6 -15
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +7 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +24 -8
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/workflow-switch-notice.mjs +94 -13
- package/src/runtime/agent/orchestrator/session/mixy-name.mjs +5 -2
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +42 -10
- package/src/runtime/agent/orchestrator/session/provider-prefix-guard.mjs +21 -3
- package/src/runtime/agent/orchestrator/session/routine-result.d.mts +35 -4
- package/src/runtime/agent/orchestrator/session/routine-result.mjs +179 -16
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/side-question-store.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/store/canonical-reader.mjs +41 -7
- package/src/runtime/agent/orchestrator/session/store/lifecycle-barriers.mjs +10 -2
- package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +14 -2
- package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +15 -1
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-record.mjs +8 -3
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +36 -1
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +40 -5
- package/src/runtime/agent/orchestrator/session/store-summary-last-message.mjs +47 -2
- package/src/runtime/agent/orchestrator/session/tail-reminder-rules.mjs +125 -0
- package/src/runtime/agent/orchestrator/session/tool-batch/finalize.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +78 -43
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +49 -14
- package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +134 -17
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +35 -13
- package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +25 -5
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/backgrounded-result.mjs +4 -3
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/completed-result.mjs +13 -14
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/transport-artifacts.mjs +9 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -14
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -1
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +6 -14
- package/src/runtime/agent/orchestrator/tools/builtin/pdf-test-fixtures.mjs +5 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +4 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-media.mjs +47 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +264 -87
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +10 -4
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +34 -21
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +8 -4
- package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +9 -30
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +24 -12
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-spawn.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/patch/delete-quarantine.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/patch/replay-capture.mjs +4 -2
- package/src/runtime/agent/orchestrator/tools/patch/rollback-state.mjs +33 -2
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +13 -13
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +5 -3
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +3 -1
- package/src/runtime/attachments/attachment-reference.mjs +155 -0
- package/src/runtime/attachments/limits.mjs +5 -1
- package/src/runtime/attachments/office-extract.mjs +3 -6
- package/src/runtime/attachments/pdf-extract.mjs +84 -10
- package/src/runtime/attachments/store.mjs +55 -30
- package/src/runtime/browser-bridge/client.mjs +73 -20
- package/src/runtime/channels/lib/runtime-paths.mjs +1 -1
- package/src/runtime/channels/lib/scheduler.mjs +14 -7
- package/src/runtime/channels/lib/state-file.mjs +15 -15
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +14 -13
- package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +22 -18
- package/src/runtime/channels/lib/webhook/session-dispatch.mjs +14 -36
- package/src/runtime/channels/lib/worker-main.mjs +3 -2
- package/src/runtime/effort-judge/compact-tokenizer.mjs +8 -2
- package/src/runtime/effort-judge/judge-client.mjs +6 -3
- package/src/runtime/effort-judge/model-install.mjs +16 -8
- package/src/runtime/effort-judge/model-manifest.json +7 -7
- package/src/runtime/local-provider/asset-installer.mjs +10 -4
- package/src/runtime/local-provider/catalog.mjs +5 -2
- package/src/runtime/local-provider/registered-models.mjs +64 -27
- package/src/runtime/local-provider/server.mjs +2 -3
- package/src/runtime/media/adapters/image-api.mjs +89 -0
- package/src/runtime/media/auth.mjs +64 -1
- package/src/runtime/media/catalog.mjs +66 -4
- package/src/runtime/media/jobs.mjs +4 -0
- package/src/runtime/media/lanes.mjs +46 -5
- package/src/runtime/media/media-usage.mjs +17 -0
- package/src/runtime/media/renditions.mjs +15 -15
- package/src/runtime/media/store.mjs +36 -35
- package/src/runtime/media/tool.mjs +8 -1
- package/src/runtime/memory/lib/cycle-scheduler/health-ledger.mjs +8 -2
- package/src/runtime/memory/lib/embedding-model-files.mjs +30 -7
- package/src/runtime/memory/lib/ingest/transcript-tail.mjs +16 -7
- package/src/runtime/memory/lib/pg/process.mjs +8 -4
- package/src/runtime/memory/lib/project-id-resolver.mjs +5 -1
- package/src/runtime/memory/lib/runtime-fetcher.mjs +6 -1
- package/src/runtime/office/core/journal.mjs +7 -14
- package/src/runtime/office/design/library/design-library-core.mjs +4 -16
- package/src/runtime/office/pdf/ocr-engine-worker.mjs +37 -0
- package/src/runtime/office/pdf/ocr-engine.mjs +146 -0
- package/src/runtime/office/pdf/pdf-ocr.mjs +51 -21
- package/src/runtime/office/pdf/pdf-render.mjs +30 -18
- package/src/runtime/office/portable/ooxml-validator.mjs +2 -2
- package/src/runtime/office/portable/portable-soffice.mjs +4 -0
- package/src/runtime/shared/agent-kind-migration.mjs +200 -0
- package/src/runtime/shared/agent-kind.mjs +152 -0
- package/src/runtime/shared/agent-message-envelope.d.mts +6 -0
- package/src/runtime/shared/agent-message-envelope.mjs +44 -0
- package/src/runtime/shared/agent-reap-state.d.mts +4 -0
- package/src/runtime/shared/agent-route-config.d.mts +1 -0
- package/src/runtime/shared/agent-route-config.mjs +22 -8
- package/src/runtime/shared/agent-store.mjs +149 -0
- package/src/runtime/shared/agent-task-starts.mjs +27 -0
- package/src/runtime/shared/atomic-file.d.mts +50 -0
- package/src/runtime/shared/atomic-file.mjs +394 -132
- package/src/runtime/shared/background-tasks.mjs +7 -4
- package/src/runtime/shared/bot-hops.mjs +30 -0
- package/src/runtime/shared/bot-split-migration.mjs +457 -0
- package/src/runtime/shared/bounded-download.mjs +4 -2
- package/src/runtime/shared/config-patch.mjs +20 -1
- package/src/runtime/shared/config.mjs +39 -9
- package/src/runtime/shared/connector-catalog.d.mts +1 -2
- package/src/runtime/shared/connector-catalog.mjs +2 -4
- package/src/runtime/shared/connector-triggers.mjs +2 -0
- package/src/runtime/shared/defaults-separation.mjs +6 -2
- package/src/runtime/shared/delegated-approval-host.mjs +74 -0
- package/src/runtime/shared/err-text.mjs +7 -1
- package/src/runtime/shared/error-presentation.mjs +113 -10
- package/src/runtime/shared/file-has-data.mjs +28 -0
- package/src/runtime/shared/handoff-artifacts.d.mts +20 -0
- package/src/runtime/shared/handoff-artifacts.mjs +61 -0
- package/src/runtime/shared/llm/provider-model-identities.mjs +6 -1
- package/src/runtime/shared/llm/usage-accounting.mjs +7 -1
- package/src/runtime/shared/llm/usage-ledger-import.mjs +37 -18
- package/src/runtime/shared/model-route-fallback.mjs +79 -7
- package/src/runtime/shared/pending-agent-work.mjs +54 -0
- package/src/runtime/shared/plain-preview.mjs +22 -17
- package/src/runtime/shared/process-lifecycle.mjs +8 -12
- package/src/runtime/shared/projects.mjs +50 -41
- package/src/runtime/shared/provider-accounts.mjs +17 -12
- package/src/runtime/shared/routine-trigger.d.mts +6 -1
- package/src/runtime/shared/routine-trigger.mjs +14 -6
- package/src/runtime/shared/run-child.d.mts +3 -0
- package/src/runtime/shared/run-child.mjs +24 -3
- package/src/runtime/shared/sensitive-path-rules.d.mts +15 -0
- package/src/runtime/shared/sensitive-path-rules.mjs +256 -0
- package/src/runtime/shared/session-baseline-store.mjs +8 -2
- package/src/runtime/shared/singleton-owner.mjs +7 -6
- package/src/runtime/shared/skill-tool-dependencies.mjs +4 -10
- package/src/runtime/shared/staged-update.mjs +4 -2
- package/src/runtime/shared/statusline/statusline-segments.mjs +4 -15
- package/src/runtime/shared/tool-execution-contract.mjs +6 -0
- package/src/runtime/shared/transcript-metadata.mjs +28 -3
- package/src/runtime/shared/transcript-writer/session-record.mjs +7 -6
- package/src/runtime/shared/turn-snapshot-store.mjs +5 -4
- package/src/runtime/shared/turn-snapshot.mjs +44 -2
- package/src/runtime/shared/usage-readout-totals.mjs +15 -13
- package/src/runtime/shared/user-cwd.mjs +9 -4
- package/src/runtime/shared/user-data-guard.mjs +46 -44
- package/src/runtime/shared/utf8-cut.mjs +10 -0
- package/src/runtime/shared/worktrees/activity-gate.mjs +269 -61
- package/src/runtime/shared/worktrees/registry.mjs +26 -26
- package/src/runtime/tidy/core-install.mjs +26 -4
- package/src/runtime/tidy/install.mjs +91 -23
- package/src/runtime/web-search/lib/config.mjs +4 -16
- package/src/session-runtime/agent-home-delivery.mjs +859 -136
- package/src/session-runtime/agent-room-delivery.mjs +167 -19
- package/src/session-runtime/agent-task-sessions.mjs +831 -69
- package/src/session-runtime/agent-tool-routing.mjs +1 -0
- package/src/session-runtime/boot/apis.mjs +58 -24
- package/src/session-runtime/boot/config.mjs +1 -0
- package/src/session-runtime/boot/facade.mjs +11 -1
- package/src/session-runtime/boot/infrastructure.mjs +14 -2
- package/src/session-runtime/boot/providers.mjs +16 -6
- package/src/session-runtime/boot/shared.mjs +12 -6
- package/src/session-runtime/boot/tools.mjs +2 -2
- package/src/session-runtime/bot-status.mjs +432 -0
- package/src/session-runtime/config-lifecycle/lead-route-sync.mjs +29 -0
- package/src/session-runtime/config-lifecycle.mjs +78 -32
- package/src/session-runtime/cwd-plugins/core-memory-context.mjs +2 -4
- package/src/session-runtime/handoff-attachments.mjs +593 -0
- package/src/session-runtime/held-requests.mjs +36 -33
- package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +10 -1
- package/src/session-runtime/internal-tool-executor/feature-tools.mjs +2 -2
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +45 -6
- package/src/session-runtime/lifecycle/session-catalog.mjs +12 -2
- package/src/session-runtime/lifecycle/teardown.mjs +11 -3
- package/src/session-runtime/mcp-glue/connect.mjs +100 -186
- package/src/session-runtime/mcp-glue/initial-connect-gate.mjs +57 -0
- package/src/session-runtime/mcp-glue/server-connection.mjs +71 -0
- package/src/session-runtime/media-api.mjs +5 -2
- package/src/session-runtime/model-route/route-persist.mjs +2 -2
- package/src/session-runtime/model-route/route-tuning.mjs +11 -2
- package/src/session-runtime/model-route/set-route.mjs +82 -2
- package/src/session-runtime/native-web-search.mjs +2 -2
- package/src/session-runtime/new-session-config.mjs +6 -1
- package/src/session-runtime/owned-automation-run.mjs +8 -1
- package/src/session-runtime/room-member-gate.mjs +16 -3
- package/src/session-runtime/route-resolution.mjs +21 -19
- package/src/session-runtime/route-state.mjs +1 -59
- package/src/session-runtime/routine-home-turn.mjs +5 -3
- package/src/session-runtime/runtime-bootstrap.mjs +5 -3
- package/src/session-runtime/runtime-paths.mjs +11 -5
- package/src/session-runtime/runtime-tunables.mjs +6 -0
- package/src/session-runtime/services/agent-tool/close-flow/close-all.mjs +11 -2
- package/src/session-runtime/services/agent-tool/close-flow.mjs +4 -2
- package/src/session-runtime/services/agent-tool/execute-send.mjs +1 -0
- package/src/session-runtime/services/agent-tool/execute-spawn.mjs +1 -0
- package/src/session-runtime/services/agent-tool/helpers.mjs +24 -5
- package/src/session-runtime/services/agent-tool/restart-notify.mjs +10 -0
- package/src/session-runtime/services/agent-tool/send-flow.mjs +36 -24
- package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +4 -0
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +6 -3
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +12 -6
- package/src/session-runtime/services/agent-tool/spawn-preset.mjs +5 -8
- package/src/session-runtime/services/agent-tool/tool-def.mjs +7 -0
- package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +22 -2
- package/src/session-runtime/services/agent-tool/worker-index.mjs +4 -6
- package/src/session-runtime/services/agent-tool.mjs +6 -0
- package/src/session-runtime/services/daemon-crash-capture/daemon-exit-record.mjs +1 -1
- package/src/session-runtime/services/daemon-crash-capture/retention.mjs +4 -3
- package/src/session-runtime/services/hook-bus/config-loader.mjs +14 -5
- package/src/session-runtime/services/hook-bus/rule-file.mjs +49 -11
- package/src/session-runtime/services/plugin-admin.mjs +26 -26
- package/src/session-runtime/services/provider-admin.mjs +14 -4
- package/src/session-runtime/services/schedule-access.mjs +33 -4
- package/src/session-runtime/services/seeds.mjs +4 -3
- package/src/session-runtime/services/usage-dashboard.mjs +1 -1
- package/src/session-runtime/services/usage-stats-model.mjs +2 -1
- package/src/session-runtime/session-create/materialize.mjs +2 -3
- package/src/session-runtime/session-create/session-options.mjs +15 -2
- package/src/session-runtime/settings-builtin-tools-api.mjs +63 -1
- package/src/session-runtime/settings-compaction-api.mjs +86 -39
- package/src/session-runtime/setup-tool/executor.mjs +20 -3
- package/src/session-runtime/setup-tool/extended-actions.mjs +19 -7
- package/src/session-runtime/setup-tool/settings-contract.mjs +37 -1
- package/src/session-runtime/setup-tool/tool-defs.mjs +20 -3
- package/src/session-runtime/skills-api.mjs +12 -5
- package/src/session-runtime/tool-policy/agent-profile-fold.mjs +265 -0
- package/src/session-runtime/tool-policy/mode-switch.mjs +54 -0
- package/src/session-runtime/tool-policy/pending-switches.mjs +59 -0
- package/src/session-runtime/tool-policy/policy-stage.mjs +165 -0
- package/src/session-runtime/tool-policy/session-switches.mjs +169 -0
- package/src/session-runtime/tool-policy/surface-helpers.mjs +114 -0
- package/src/session-runtime/tool-policy/workflow-overlay.mjs +145 -0
- package/src/session-runtime/tool-policy-refresh.mjs +47 -358
- package/src/session-runtime/tool-surface.mjs +5 -3
- package/src/session-runtime/turn/turn-run/turn-prepare.mjs +99 -81
- package/src/session-runtime/turn/turn-run/turn-timing.mjs +4 -3
- package/src/session-runtime/turn/turn-run.mjs +2 -1
- package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +54 -7
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +578 -301
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +113 -53
- package/src/session-runtime/workflow-agents-api/onboarding.mjs +11 -4
- package/src/session-runtime/workflow-agents-api/shared.mjs +5 -1
- package/src/session-runtime/workflow-agents-api/style-and-mode.mjs +14 -1
- package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +6 -3
- package/src/session-runtime/workspace/workspace-state.mjs +7 -6
- package/src/standalone/daemon-agent-control.mjs +4 -1
- package/src/standalone/daemon-session-control.mjs +60 -11
- package/src/standalone/daemon.mjs +34 -4
- package/src/standalone/local-session-runtime.mjs +22 -14
- package/src/standalone/session-client.mjs +7 -2
- package/src/standalone/session-protocol.mjs +7 -0
- package/src/standalone/session-service/agent-tree/agent-turns.mjs +46 -3
- package/src/standalone/session-service/agent-tree.mjs +4 -3
- package/src/standalone/session-service/session-calls/session-turns.mjs +19 -0
- package/src/standalone/session-service.mjs +11 -0
- package/src/standalone/session-state-projection.mjs +19 -9
- package/src/standalone/session-tool-output.mjs +35 -9
- package/src/tui/app/maintenance-pickers/auto-clear-picker.mjs +19 -13
- package/src/tui/app/settings-picker/settings-rows.mjs +2 -2
- package/src/tui/app/slash-commands.mjs +1 -1
- package/src/tui/app/usage-context-panels/context-model.mjs +8 -2
- package/src/tui/dist/index.mjs +599 -342
- package/src/tui/prompt-history-store.mjs +16 -21
- package/src/tui/session/context-state.mjs +4 -0
- package/src/tui/session/notice-surface.mjs +3 -4
- package/src/tui/session/session-api/intake/abort.mjs +19 -1
- package/src/tui/session/session-api/media.mjs +2 -1
- package/src/tui/session/session-api/routes-agent-definitions.mjs +75 -0
- package/src/tui/session/session-api/routes-write.mjs +203 -0
- package/src/tui/session/session-api/routes.mjs +23 -236
- package/src/tui/session/session-api/settings.mjs +14 -1
- package/src/tui/session/session-flow/drain.mjs +28 -3
- package/src/tui/session/session-flow/failed-turn-rewind.mjs +4 -2
- package/src/tui/session/session-flow/queue/take-entries.mjs +8 -0
- package/src/tui/session/session-flow/steering.mjs +8 -3
- package/src/tui/session/tool-approval.mjs +185 -20
- package/src/tui/session/transcript-intake.mjs +4 -0
- package/src/tui/session/turn-live-call.mjs +11 -3
- package/src/tui/session/turn.mjs +36 -17
- package/src/tui/transcript-tool-failures.mjs +8 -2
- package/src/vendor/statusline/src/gateway/route-meta.mjs +7 -8
- package/src/vendor/statusline/src/gateway/session-routes.mjs +6 -7
- package/src/rules/agent-mode/ROUTINE.md +0 -6
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// Embedding model files in the transformers.js cache layout
|
|
2
2
|
// (<cache>/<model id>/<path>), so copies that library already downloaded are
|
|
3
3
|
// reused; a missing file is fetched from the Hugging Face Hub once.
|
|
4
|
-
import { createWriteStream, existsSync, mkdirSync, renameSync, rmSync } from 'node:fs';
|
|
4
|
+
import { createWriteStream, existsSync, mkdirSync, readFileSync, renameSync, rmSync } from 'node:fs';
|
|
5
5
|
import { dirname, join } from 'node:path';
|
|
6
6
|
import { Readable } from 'node:stream';
|
|
7
7
|
import { pipeline } from 'node:stream/promises';
|
|
8
|
+
import { parseJsonText } from '../../shared/atomic-file.mjs';
|
|
9
|
+
import { fileHasData } from '../../shared/file-has-data.mjs';
|
|
8
10
|
|
|
9
11
|
const HUB = 'https://huggingface.co';
|
|
10
12
|
|
|
@@ -17,7 +19,8 @@ async function download(modelId, file, target, { optional = false } = {}) {
|
|
|
17
19
|
mkdirSync(dirname(target), { recursive: true });
|
|
18
20
|
const partial = `${target}.${process.pid}.part`;
|
|
19
21
|
try {
|
|
20
|
-
|
|
22
|
+
// flush: fsync before close, so the renamed file cannot be all NUL after a crash.
|
|
23
|
+
await pipeline(Readable.fromWeb(res.body), createWriteStream(partial, { flush: true }));
|
|
21
24
|
renameSync(partial, target);
|
|
22
25
|
} catch (error) {
|
|
23
26
|
rmSync(partial, { force: true });
|
|
@@ -26,19 +29,39 @@ async function download(modelId, file, target, { optional = false } = {}) {
|
|
|
26
29
|
return true;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
// A cached file that holds no data (empty, NUL-filled by a crash) or, for
|
|
33
|
+
// JSON, does not parse is not present: it is fetched again.
|
|
34
|
+
function cachedFileUsable(target) {
|
|
35
|
+
if (!fileHasData(target)) return false;
|
|
36
|
+
if (!target.endsWith('.json')) return true;
|
|
37
|
+
try {
|
|
38
|
+
parseJsonText(readFileSync(target, 'utf8'));
|
|
39
|
+
return true;
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Local path of `file`, downloading it first when absent or damaged. */
|
|
30
46
|
export async function ensureModelFile(cacheDir, modelId, file) {
|
|
31
47
|
const target = join(cacheDir, ...modelId.split('/'), ...file.split('/'));
|
|
32
|
-
if (!
|
|
48
|
+
if (!cachedFileUsable(target)) await download(modelId, file, target);
|
|
33
49
|
return target;
|
|
34
50
|
}
|
|
35
51
|
|
|
36
52
|
/** Local path of an ONNX graph; a graph published with external weights gets them alongside. */
|
|
37
53
|
export async function ensureModelGraph(cacheDir, modelId, file) {
|
|
38
54
|
const target = join(cacheDir, ...modelId.split('/'), ...file.split('/'));
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
55
|
+
const weights = `${target}_data`;
|
|
56
|
+
const graphPresent = fileHasData(target);
|
|
57
|
+
// The weights are checked on their own: an external-weights file a crash
|
|
58
|
+
// left empty or NUL-filled is fetched again even when the graph is intact.
|
|
59
|
+
// They are fetched before the graph, so a cached graph implies its weights
|
|
60
|
+
// finished downloading; a weights file that is absent stays absent (graphs
|
|
61
|
+
// without external data have none).
|
|
62
|
+
if (!graphPresent || (existsSync(weights) && !fileHasData(weights))) {
|
|
63
|
+
await download(modelId, `${file}_data`, weights, { optional: true });
|
|
42
64
|
}
|
|
65
|
+
if (!graphPresent) await download(modelId, file, target);
|
|
43
66
|
return target;
|
|
44
67
|
}
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
// One incremental read of a transcript file: the bytes past the persisted
|
|
3
3
|
// offset, split into complete JSONL lines, stored as conversation rows. The
|
|
4
4
|
// persisted boundary only advances past lines that were fully consumed (parsed
|
|
5
|
-
// + inserted or intentionally skipped); a
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
5
|
+
// + inserted or intentionally skipped); a transient insert error stops the walk
|
|
6
|
+
// and leaves the boundary untouched so the next sweep retries from the same
|
|
7
|
+
// position instead of silently consuming the line forever. A complete line
|
|
8
|
+
// that is not JSON (crash damage) is skipped, never a permanent stop.
|
|
9
9
|
import fs from 'node:fs';
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { cwdFromTranscriptPath, transcriptRowFromLine } from './transcript-rows.mjs';
|
|
@@ -73,8 +73,12 @@ export async function ingestTranscriptTail({ db, transcriptPath, cwd, offsets, r
|
|
|
73
73
|
const boundary = { bytes: tail.bytes, lineIndex: tail.lineIndex, generation: tail.generation };
|
|
74
74
|
let index = tail.lineIndex;
|
|
75
75
|
let count = 0;
|
|
76
|
-
for (const { line, consumedBytes } of completeLines(tail.text)) {
|
|
77
|
-
|
|
76
|
+
for (const { line: rawLine, consumedBytes } of completeLines(tail.text)) {
|
|
77
|
+
// A crash leaves a run of NUL bytes where data never reached disk, and the
|
|
78
|
+
// resumed writer appends after it. A raw NUL is never valid JSON, so drop
|
|
79
|
+
// them and keep the record that follows.
|
|
80
|
+
const line = rawLine.includes('\0') ? rawLine.replaceAll('\0', '') : rawLine;
|
|
81
|
+
if (!line.trim()) {
|
|
78
82
|
boundary.bytes += consumedBytes;
|
|
79
83
|
continue;
|
|
80
84
|
}
|
|
@@ -82,7 +86,12 @@ export async function ingestTranscriptTail({ db, transcriptPath, cwd, offsets, r
|
|
|
82
86
|
try {
|
|
83
87
|
parsed = JSON.parse(line);
|
|
84
88
|
} catch {
|
|
85
|
-
|
|
89
|
+
// The line is complete (a half-written one has no newline yet and is not
|
|
90
|
+
// yielded), so it can never become valid: skip it for good, or it would
|
|
91
|
+
// block every line after it.
|
|
92
|
+
log(`[transcript-watch] skipped a damaged line in ${path.basename(transcriptPath)}\n`);
|
|
93
|
+
boundary.bytes += consumedBytes;
|
|
94
|
+
continue;
|
|
86
95
|
}
|
|
87
96
|
index += 1;
|
|
88
97
|
const row = transcriptRowFromLine(parsed);
|
|
@@ -26,6 +26,7 @@ import { readdir, readFile, rm, stat } from 'node:fs/promises';
|
|
|
26
26
|
import { join } from 'node:path';
|
|
27
27
|
import { execFile, spawn, spawnSync } from 'node:child_process';
|
|
28
28
|
import { createConnection, createServer } from 'node:net';
|
|
29
|
+
import { holdsNoData, writeFileAtomicSync } from '../../../shared/atomic-file.mjs';
|
|
29
30
|
import { isPidAlive } from '../../../shared/pid-liveness.mjs';
|
|
30
31
|
import { runChild } from './run-child.mjs';
|
|
31
32
|
|
|
@@ -180,9 +181,12 @@ function stripPlatformInvalidLines(conf) {
|
|
|
180
181
|
function ensureConfV2(pgdataDir) {
|
|
181
182
|
const confPath = join(pgdataDir, 'postgresql.conf');
|
|
182
183
|
if (!existsSync(confPath)) return false;
|
|
183
|
-
|
|
184
|
+
// A conf a crash left empty or NUL-filled cannot be parsed by PostgreSQL; the
|
|
185
|
+
// defaults plus the overrides appended below are a working config.
|
|
186
|
+
const text = readFileSync(confPath, 'utf8');
|
|
187
|
+
const original = holdsNoData(text) ? '' : text;
|
|
184
188
|
let conf = stripPlatformInvalidLines(original);
|
|
185
|
-
let changed = conf !==
|
|
189
|
+
let changed = conf !== text;
|
|
186
190
|
if (!conf.includes(MIXDOG_CONF_V2_MARKER)) {
|
|
187
191
|
conf += buildV2Block();
|
|
188
192
|
changed = true;
|
|
@@ -196,7 +200,7 @@ function ensureConfV2(pgdataDir) {
|
|
|
196
200
|
changed = true;
|
|
197
201
|
}
|
|
198
202
|
if (!changed) return false;
|
|
199
|
-
|
|
203
|
+
writeFileAtomicSync(confPath, conf, { backup: true });
|
|
200
204
|
return true;
|
|
201
205
|
}
|
|
202
206
|
|
|
@@ -350,7 +354,7 @@ async function initClusterIfNeeded({ runtimeDir, pgdataDir, env }) {
|
|
|
350
354
|
const confPath = join(pgdataDir, 'postgresql.conf');
|
|
351
355
|
try {
|
|
352
356
|
const existing = readFileSync(confPath, 'utf8');
|
|
353
|
-
|
|
357
|
+
writeFileAtomicSync(confPath, existing + freshClusterConfAppend());
|
|
354
358
|
} catch (e) {
|
|
355
359
|
__mixdogMemoryLog(`[pg-process] postgresql.conf append failed: ${e?.message}\n`);
|
|
356
360
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join, dirname, resolve } from 'node:path';
|
|
3
3
|
import { explicitSessionCwd } from '../../shared/user-cwd.mjs';
|
|
4
|
+
import { holdsNoData } from '../../shared/atomic-file.mjs';
|
|
4
5
|
|
|
5
6
|
/** @type {Map<string, string|null>} */
|
|
6
7
|
const cache = new Map();
|
|
@@ -51,7 +52,10 @@ export function resolveProjectId(cwd) {
|
|
|
51
52
|
const idFile = join(mixdogRoot, '.mixdog', 'project.id');
|
|
52
53
|
let content;
|
|
53
54
|
try {
|
|
54
|
-
|
|
55
|
+
const raw = readFileSync(idFile, 'utf8');
|
|
56
|
+
// A crash can leave the marker NUL-filled; trim() keeps NULs, so it would
|
|
57
|
+
// otherwise become the project id. No data means no marker (COMMON).
|
|
58
|
+
content = holdsNoData(raw) ? '' : raw.trim();
|
|
55
59
|
} catch {
|
|
56
60
|
// TOCTOU: the file vanished between findProjectIdRoot's existsSync and
|
|
57
61
|
// this read. Treat as no marker; do not cache so a transient miss can
|
|
@@ -34,6 +34,7 @@ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'nod
|
|
|
34
34
|
import { fileURLToPath } from 'node:url';
|
|
35
35
|
import { runChild } from './pg/run-child.mjs';
|
|
36
36
|
import { renameWithRetrySync, writeFileAtomicSync, writeJsonAtomicSync } from '../../shared/atomic-file.mjs';
|
|
37
|
+
import { fileHasData } from '../../shared/file-has-data.mjs';
|
|
37
38
|
import { downloadToFileWithRetry } from '../../shared/bounded-download.mjs';
|
|
38
39
|
import { platformKey, platformKeyCandidates, verifySha256File } from '../../shared/native-asset.mjs';
|
|
39
40
|
|
|
@@ -497,8 +498,12 @@ function cachedRuntimeDir(runtimeBaseDir, version, sha256) {
|
|
|
497
498
|
}
|
|
498
499
|
|
|
499
500
|
// The asset digest stamped into a complete runtime tree, '' when absent.
|
|
501
|
+
// A tree whose postgres executable holds no data (a crash after the extract
|
|
502
|
+
// left it NUL-filled) is not complete, whatever its stamp says.
|
|
500
503
|
function readRuntimeStamp(verDir) {
|
|
501
504
|
try {
|
|
505
|
+
const postgres = join(verDir, 'bin', process.platform === 'win32' ? 'postgres.exe' : 'postgres');
|
|
506
|
+
if (existsSync(postgres) && !fileHasData(postgres)) return '';
|
|
502
507
|
return readFileSync(join(verDir, '.version-sha256'), 'utf8').trim();
|
|
503
508
|
} catch {
|
|
504
509
|
return '';
|
|
@@ -588,7 +593,7 @@ async function installRuntimeVersion({ runtimeBaseDir, url, sha256, size, versio
|
|
|
588
593
|
}
|
|
589
594
|
|
|
590
595
|
// Stamp sha256 inside staging dir.
|
|
591
|
-
|
|
596
|
+
writeFileAtomicSync(join(stagingDir, '.version-sha256'), sha256);
|
|
592
597
|
normalizeBinExecBit(stagingDir);
|
|
593
598
|
|
|
594
599
|
// Atomic swap:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { mkdir, readFile, readdir,
|
|
1
|
+
import { mkdir, readFile, readdir, rm, stat } from 'node:fs/promises';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join, resolve } from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { parseJsonText, writeJsonAtomicAsync } from '../../shared/atomic-file.mjs';
|
|
5
5
|
|
|
6
6
|
const RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
|
|
7
7
|
|
|
@@ -18,17 +18,8 @@ function journalPath(id, dataDir) {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export async function writeOfficeJournal(record, dataDir = '') {
|
|
21
|
-
const directory = officeJournalDirectory(dataDir);
|
|
22
|
-
await mkdir(directory, { recursive: true });
|
|
23
21
|
const target = journalPath(record.id, dataDir);
|
|
24
|
-
|
|
25
|
-
await writeFile(temporary, `${JSON.stringify(record, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
26
|
-
try {
|
|
27
|
-
await rename(temporary, target);
|
|
28
|
-
} catch {
|
|
29
|
-
await rm(target, { force: true });
|
|
30
|
-
await rename(temporary, target);
|
|
31
|
-
}
|
|
22
|
+
await writeJsonAtomicAsync(target, record);
|
|
32
23
|
return target;
|
|
33
24
|
}
|
|
34
25
|
|
|
@@ -38,9 +29,11 @@ export async function removeOfficeJournal(id, dataDir = '') {
|
|
|
38
29
|
|
|
39
30
|
export async function readOfficeJournal(id, dataDir = '') {
|
|
40
31
|
try {
|
|
41
|
-
return
|
|
32
|
+
return parseJsonText(await readFile(journalPath(id, dataDir), 'utf8'));
|
|
42
33
|
} catch (error) {
|
|
43
|
-
|
|
34
|
+
// A journal a crash left empty or NUL-filled records nothing to recover;
|
|
35
|
+
// it stays on disk for the retention sweep and never blocks the caller.
|
|
36
|
+
if (error?.code === 'ENOENT' || error instanceof SyntaxError) return null;
|
|
44
37
|
throw error;
|
|
45
38
|
}
|
|
46
39
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { writeJsonAtomicAsync } from '../../../shared/atomic-file.mjs';
|
|
3
3
|
import { delimiter, dirname, isAbsolute, join, resolve, win32 } from 'node:path';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
@@ -126,20 +126,8 @@ export async function readJson(path, fallback = null) {
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
export async function writeJsonAtomic(path, value) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, {
|
|
132
|
-
encoding: 'utf8',
|
|
133
|
-
mode: 0o600,
|
|
134
|
-
});
|
|
135
|
-
try {
|
|
136
|
-
await rename(temporary, path);
|
|
137
|
-
} catch {
|
|
138
|
-
await rm(path, { force: true });
|
|
139
|
-
await rename(temporary, path);
|
|
140
|
-
} finally {
|
|
141
|
-
await rm(temporary, { force: true }).catch(() => {});
|
|
142
|
-
}
|
|
129
|
+
// Durable atomic replace; the replaced version stays as <file>.bak.
|
|
130
|
+
await writeJsonAtomicAsync(path, value, { backup: true });
|
|
143
131
|
}
|
|
144
132
|
|
|
145
133
|
function resolveConfigPath(path, base) {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// The OCR engine's own thread. Starting the engine includes the first-use
|
|
2
|
+
// download of each language's data and gives no handle until it finishes, so
|
|
3
|
+
// it runs here, where ending this thread ends the engine and the download with
|
|
4
|
+
// it (ocr-engine.mjs).
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
import { mkdir } from 'node:fs/promises';
|
|
7
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
8
|
+
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
const { languages, cachePath, langPath } = workerData;
|
|
11
|
+
|
|
12
|
+
let engine = null;
|
|
13
|
+
try {
|
|
14
|
+
await mkdir(cachePath, { recursive: true });
|
|
15
|
+
const tesseract = require('tesseract.js');
|
|
16
|
+
engine = await tesseract.createWorker(languages, tesseract.OEM.LSTM_ONLY, {
|
|
17
|
+
cachePath,
|
|
18
|
+
gzip: true,
|
|
19
|
+
...(langPath ? { langPath } : {}),
|
|
20
|
+
});
|
|
21
|
+
} catch (error) {
|
|
22
|
+
parentPort.postMessage({ startError: String(error?.message || error) });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (engine) {
|
|
26
|
+
// One page at a time, in arrival order: the engine itself is serial.
|
|
27
|
+
parentPort.on('message', async ({ id, image, output }) => {
|
|
28
|
+
try {
|
|
29
|
+
const input = typeof image === 'string' ? image : Buffer.from(image.buffer, image.byteOffset, image.byteLength);
|
|
30
|
+
const recognized = await engine.recognize(input, {}, output || { text: true });
|
|
31
|
+
parentPort.postMessage({ id, data: recognized.data });
|
|
32
|
+
} catch (error) {
|
|
33
|
+
parentPort.postMessage({ id, error: String(error?.message || error) });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
parentPort.postMessage({ ready: true });
|
|
37
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// One OCR engine per language set and cache, on a thread of its own so that a
|
|
2
|
+
// stalled start (the first-use language download) or a cancelled page can be
|
|
3
|
+
// ended at once: ending the thread ends the engine and its download. Callers
|
|
4
|
+
// arriving while it starts share that start; the engine ends when the last
|
|
5
|
+
// caller releases it.
|
|
6
|
+
import { Worker } from 'node:worker_threads';
|
|
7
|
+
import { workerExecArgv } from '../../shared/worker-exec-argv.mjs';
|
|
8
|
+
|
|
9
|
+
const ENGINE_URL = new URL('./ocr-engine-worker.mjs', import.meta.url);
|
|
10
|
+
|
|
11
|
+
/** The start includes the first-use download; without network it fails or hangs. */
|
|
12
|
+
export const OCR_ENGINE_START_TIMEOUT_MS = 90_000;
|
|
13
|
+
|
|
14
|
+
const engines = new Map();
|
|
15
|
+
|
|
16
|
+
const cancelledError = () => new Error('PDF OCR was cancelled');
|
|
17
|
+
const unavailableError = (message) => Object.assign(new Error(message), { code: 'OCR_UNAVAILABLE' });
|
|
18
|
+
|
|
19
|
+
const defaultSpawn = (url, options) => new Worker(url, options);
|
|
20
|
+
|
|
21
|
+
/** How many engine threads are running (for diagnostics and tests). */
|
|
22
|
+
export function liveOcrEngines() {
|
|
23
|
+
return engines.size;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Ends the engine: its thread, its start and every page it still owes.
|
|
27
|
+
function closeEngine(entry, error) {
|
|
28
|
+
if (entry.closed) return;
|
|
29
|
+
entry.closed = true;
|
|
30
|
+
entry.error = error;
|
|
31
|
+
clearTimeout(entry.timer);
|
|
32
|
+
if (engines.get(entry.key) === entry) engines.delete(entry.key);
|
|
33
|
+
entry.failStart(error);
|
|
34
|
+
const jobs = [...entry.jobs.values()];
|
|
35
|
+
entry.jobs.clear();
|
|
36
|
+
for (const job of jobs) job.reject(error);
|
|
37
|
+
void entry.thread.terminate().catch(() => {});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function startEngine(key, { languages, cachePath, langPath, timeoutMs, spawn }) {
|
|
41
|
+
const entry = { key, holders: 0, jobs: new Map(), nextJob: 0, closed: false, error: null };
|
|
42
|
+
entry.started = new Promise((resolve, reject) => {
|
|
43
|
+
entry.markStarted = resolve;
|
|
44
|
+
entry.failStart = reject;
|
|
45
|
+
});
|
|
46
|
+
entry.started.catch(() => {});
|
|
47
|
+
entry.thread = spawn(ENGINE_URL, {
|
|
48
|
+
execArgv: workerExecArgv(),
|
|
49
|
+
workerData: { languages, cachePath, langPath: langPath || null },
|
|
50
|
+
});
|
|
51
|
+
entry.timer = setTimeout(
|
|
52
|
+
() => closeEngine(entry, unavailableError('OCR language data did not load in time (it downloads on first use)')),
|
|
53
|
+
timeoutMs
|
|
54
|
+
);
|
|
55
|
+
entry.timer.unref?.();
|
|
56
|
+
entry.thread.on('message', (message) => {
|
|
57
|
+
if (message?.ready) {
|
|
58
|
+
clearTimeout(entry.timer);
|
|
59
|
+
entry.markStarted();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (message?.startError) {
|
|
63
|
+
closeEngine(entry, unavailableError(`OCR could not start: ${message.startError}`));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const job = entry.jobs.get(message?.id);
|
|
67
|
+
if (!job) return;
|
|
68
|
+
entry.jobs.delete(message.id);
|
|
69
|
+
if (message.error) job.reject(new Error(`OCR failed: ${message.error}`));
|
|
70
|
+
else job.resolve(message.data);
|
|
71
|
+
});
|
|
72
|
+
entry.thread.on('error', (error) => closeEngine(entry, error));
|
|
73
|
+
entry.thread.on('exit', (code) => closeEngine(entry, new Error(`OCR engine stopped (exit ${code})`)));
|
|
74
|
+
engines.set(key, entry);
|
|
75
|
+
return entry;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// `promise`, or a cancellation as soon as `signal` aborts.
|
|
79
|
+
function untilAborted(promise, signal) {
|
|
80
|
+
if (!signal) return promise;
|
|
81
|
+
if (signal.aborted) return Promise.reject(cancelledError());
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
const onAbort = () => reject(cancelledError());
|
|
84
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
85
|
+
promise.then(resolve, reject).finally(() => signal.removeEventListener('abort', onAbort));
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function recognizeOn(entry, image, { output, signal } = {}) {
|
|
90
|
+
if (entry.closed) return Promise.reject(entry.error);
|
|
91
|
+
if (signal?.aborted) return Promise.reject(cancelledError());
|
|
92
|
+
const id = ++entry.nextJob;
|
|
93
|
+
const answer = new Promise((resolve, reject) => {
|
|
94
|
+
entry.jobs.set(id, { resolve, reject });
|
|
95
|
+
});
|
|
96
|
+
entry.thread.postMessage({ id, image, output: output || null });
|
|
97
|
+
if (!signal) return answer;
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
const onAbort = () => {
|
|
100
|
+
entry.jobs.delete(id);
|
|
101
|
+
// A page in recognition cannot be stopped inside the engine: the engine
|
|
102
|
+
// ends with it unless another caller still uses it, which then gets the
|
|
103
|
+
// page's (discarded) answer before its own.
|
|
104
|
+
if (entry.holders <= 1) closeEngine(entry, cancelledError());
|
|
105
|
+
reject(cancelledError());
|
|
106
|
+
};
|
|
107
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
108
|
+
answer.then(resolve, reject).finally(() => signal.removeEventListener('abort', onAbort));
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The engine for `languages` ("eng+kor") with its data cached at `cachePath`,
|
|
114
|
+
* started on first use. Resolves `{ recognize(image, { output, signal }),
|
|
115
|
+
* release() }` once it is ready; the caller releases it when done. Rejects
|
|
116
|
+
* with code OCR_UNAVAILABLE when it does not start within `timeoutMs`, and
|
|
117
|
+
* with a cancellation when `signal` aborts, ending the start if no one else
|
|
118
|
+
* waits on it.
|
|
119
|
+
*/
|
|
120
|
+
export async function acquireOcrEngine({
|
|
121
|
+
languages,
|
|
122
|
+
cachePath,
|
|
123
|
+
langPath = null,
|
|
124
|
+
signal = null,
|
|
125
|
+
timeoutMs = OCR_ENGINE_START_TIMEOUT_MS,
|
|
126
|
+
spawn = defaultSpawn,
|
|
127
|
+
}) {
|
|
128
|
+
if (signal?.aborted) throw cancelledError();
|
|
129
|
+
const key = `${cachePath}\0${languages}\0${langPath || ''}`;
|
|
130
|
+
const entry = engines.get(key) || startEngine(key, { languages, cachePath, langPath, timeoutMs, spawn });
|
|
131
|
+
entry.holders += 1;
|
|
132
|
+
let released = false;
|
|
133
|
+
const release = () => {
|
|
134
|
+
if (released) return;
|
|
135
|
+
released = true;
|
|
136
|
+
entry.holders -= 1;
|
|
137
|
+
if (entry.holders <= 0) closeEngine(entry, new Error('OCR engine released'));
|
|
138
|
+
};
|
|
139
|
+
try {
|
|
140
|
+
await untilAborted(entry.started, signal);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
release();
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
return { recognize: (image, options) => recognizeOn(entry, image, options), release };
|
|
146
|
+
}
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
// PDF OCR: tesseract TSV/block parsing, readiness probe and the ocr operation.
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
-
import {
|
|
3
|
+
import { readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { PDFDocument, rgb } from 'pdf-lib';
|
|
6
6
|
import { SAVE_OPTIONS } from './pdf-draw.mjs';
|
|
7
7
|
import { embedDocumentFont, fontCovers } from './pdf-fonts.mjs';
|
|
8
|
-
import { renderPdfPages } from './pdf-render.mjs';
|
|
8
|
+
import { PDF_RENDER_MAX_EXPLICIT_PAGES, renderPdfPages } from './pdf-render.mjs';
|
|
9
9
|
import { selectedPages } from './pdf-document.mjs';
|
|
10
|
+
import { acquireOcrEngine } from './ocr-engine.mjs';
|
|
10
11
|
|
|
11
12
|
const require = createRequire(import.meta.url);
|
|
12
13
|
|
|
14
|
+
const DEFAULT_MIN_CONFIDENCE = 40;
|
|
15
|
+
|
|
13
16
|
// The engine's own column order. Tesseract writes this table with a header
|
|
14
17
|
// row through its command line and without one through the worker API, so the
|
|
15
18
|
// reader accepts both: taking the first data row as a header dropped that word
|
|
@@ -208,7 +211,7 @@ export async function pdfOcrReadiness(dataDir) {
|
|
|
208
211
|
|
|
209
212
|
// Renders and recognizes one page; the rendered image path is recorded on
|
|
210
213
|
// `temporaryImages` for cleanup.
|
|
211
|
-
async function recognizePage(
|
|
214
|
+
async function recognizePage(engine, path, pageNumber, operation, signal, temporaryImages) {
|
|
212
215
|
const rendered = await renderPdfPages(path, {
|
|
213
216
|
pages: [pageNumber],
|
|
214
217
|
maxWidth: Math.max(1200, Math.min(3200, Number(operation.maxWidth) || 2400)),
|
|
@@ -216,13 +219,13 @@ async function recognizePage(worker, path, pageNumber, operation, signal, tempor
|
|
|
216
219
|
});
|
|
217
220
|
const image = rendered.images[0];
|
|
218
221
|
temporaryImages.push(image.path);
|
|
219
|
-
const recognized = await
|
|
220
|
-
const lines = ocrTextLines(recognized.
|
|
221
|
-
const minConfidence = Number(operation.minConfidence ??
|
|
222
|
-
const words = (lines.length ? lines.flatMap((line) => line.words) : parseOcrBlocks(recognized.
|
|
222
|
+
const recognized = await engine.recognize(image.path, { output: { text: true, tsv: true, blocks: true }, signal });
|
|
223
|
+
const lines = ocrTextLines(recognized.tsv, recognized.text);
|
|
224
|
+
const minConfidence = Number(operation.minConfidence ?? DEFAULT_MIN_CONFIDENCE);
|
|
225
|
+
const words = (lines.length ? lines.flatMap((line) => line.words) : parseOcrBlocks(recognized.blocks)).filter(
|
|
223
226
|
(word) => word.confidence >= minConfidence
|
|
224
227
|
);
|
|
225
|
-
return { pageNumber, image, lines, words, text: recognized.
|
|
228
|
+
return { pageNumber, image, lines, words, text: recognized.text || '' };
|
|
226
229
|
}
|
|
227
230
|
|
|
228
231
|
// One font for all the recognized text: Helvetica when it is Latin, an
|
|
@@ -285,18 +288,15 @@ function placeRecognizedText(document, { pageNumber, image, lines, words }, font
|
|
|
285
288
|
}
|
|
286
289
|
}
|
|
287
290
|
|
|
288
|
-
|
|
291
|
+
// The shared engine for the operation's languages (ocr-engine.mjs): it starts
|
|
292
|
+
// on its own thread, so a stalled first-use download or a cancellation ends
|
|
293
|
+
// it at once.
|
|
294
|
+
async function openOcrEngine(operation, dataDir, signal) {
|
|
289
295
|
const languages = Array.isArray(operation.languages)
|
|
290
296
|
? operation.languages.map(String).join('+')
|
|
291
297
|
: String(operation.languages || 'eng+kor');
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
const tesseract = require('tesseract.js');
|
|
295
|
-
const worker = await tesseract.createWorker(languages, tesseract.OEM.LSTM_ONLY, {
|
|
296
|
-
cachePath,
|
|
297
|
-
gzip: true,
|
|
298
|
-
});
|
|
299
|
-
return { worker, languages };
|
|
298
|
+
const engine = await acquireOcrEngine({ languages, cachePath: ocrCachePath(dataDir), signal });
|
|
299
|
+
return { engine, languages };
|
|
300
300
|
}
|
|
301
301
|
|
|
302
302
|
function ocrResult(operation, { pages, languages, text, tally, fontPath, embedded }) {
|
|
@@ -318,6 +318,36 @@ function ocrResult(operation, { pages, languages, text, tally, fontPath, embedde
|
|
|
318
318
|
};
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
+
/**
|
|
322
|
+
* Read-only OCR: the recognized text of `pages` (1-based) of the PDF at
|
|
323
|
+
* `path`, as `[{ page, text }]`. Pages are rendered in memory and nothing is
|
|
324
|
+
* written next to the PDF or into it. Throws when the engine or its language
|
|
325
|
+
* data is unavailable (no network on first use).
|
|
326
|
+
*/
|
|
327
|
+
export async function recognizePdfPageTexts(path, pages, { dataDir, signal = null, languages } = {}) {
|
|
328
|
+
const { engine } = await openOcrEngine({ languages }, dataDir, signal);
|
|
329
|
+
const out = [];
|
|
330
|
+
try {
|
|
331
|
+
for (let offset = 0; offset < pages.length; offset += PDF_RENDER_MAX_EXPLICIT_PAGES) {
|
|
332
|
+
if (signal?.aborted) throw new Error('PDF OCR was cancelled');
|
|
333
|
+
const rendered = await renderPdfPages(path, {
|
|
334
|
+
pages: pages.slice(offset, offset + PDF_RENDER_MAX_EXPLICIT_PAGES),
|
|
335
|
+
maxWidth: 2000,
|
|
336
|
+
signal,
|
|
337
|
+
writeImages: false,
|
|
338
|
+
});
|
|
339
|
+
for (const image of rendered.images) {
|
|
340
|
+
if (signal?.aborted) throw new Error('PDF OCR was cancelled');
|
|
341
|
+
const recognized = await engine.recognize(Buffer.from(image.data, 'base64'), { signal });
|
|
342
|
+
out.push({ page: image.page, text: String(recognized?.text || '').trim() });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return out;
|
|
346
|
+
} finally {
|
|
347
|
+
engine.release();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
321
351
|
export async function ocrPdf(path, operation, { dataDir, signal = null } = {}) {
|
|
322
352
|
const source = await readFile(path);
|
|
323
353
|
const document = await PDFDocument.load(source, {
|
|
@@ -325,7 +355,7 @@ export async function ocrPdf(path, operation, { dataDir, signal = null } = {}) {
|
|
|
325
355
|
updateMetadata: false,
|
|
326
356
|
});
|
|
327
357
|
const pages = selectedPages(document.getPageCount(), operation.pages || (operation.page ? [operation.page] : null));
|
|
328
|
-
const {
|
|
358
|
+
const { engine, languages } = await openOcrEngine(operation, dataDir, signal);
|
|
329
359
|
const tally = { wordCount: 0, skippedWords: 0, totalConfidence: 0 };
|
|
330
360
|
let text = '';
|
|
331
361
|
const temporaryImages = [];
|
|
@@ -335,19 +365,19 @@ export async function ocrPdf(path, operation, { dataDir, signal = null } = {}) {
|
|
|
335
365
|
// it produced.
|
|
336
366
|
for (const pageNumber of pages) {
|
|
337
367
|
if (signal?.aborted) throw new Error('PDF OCR was cancelled');
|
|
338
|
-
const recognized = await recognizePage(
|
|
368
|
+
const recognized = await recognizePage(engine, path, pageNumber, operation, signal, temporaryImages);
|
|
339
369
|
recognizedPages.push(recognized);
|
|
340
370
|
text += `${text ? '\n\n' : ''}--- Page ${pageNumber} ---\n${recognized.text}`;
|
|
341
371
|
}
|
|
342
372
|
const { font, fontPath, embedded } = await selectOcrFont(document, operation, recognizedPages);
|
|
343
|
-
const minConfidence = Number(operation.minConfidence ??
|
|
373
|
+
const minConfidence = Number(operation.minConfidence ?? DEFAULT_MIN_CONFIDENCE);
|
|
344
374
|
for (const entry of recognizedPages) placeRecognizedText(document, entry, font, minConfidence, tally);
|
|
345
375
|
if (tally.wordCount > 0) {
|
|
346
376
|
await writeFile(path, await document.save(SAVE_OPTIONS));
|
|
347
377
|
}
|
|
348
378
|
return ocrResult(operation, { pages, languages, text, tally, fontPath, embedded });
|
|
349
379
|
} finally {
|
|
350
|
-
|
|
380
|
+
engine.release();
|
|
351
381
|
for (const image of temporaryImages) await rm(image, { force: true }).catch(() => {});
|
|
352
382
|
}
|
|
353
383
|
}
|