mixdog 1.2.6 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSES/simple-icons-CC0-1.0.txt +30 -0
- package/NOTICE.md +71 -9
- package/package.json +4 -1
- package/scripts/check-model-recommendations.mjs +89 -0
- package/scripts/gate-local.mjs +7 -0
- package/scripts/lib/isolated-test-env.mjs +16 -0
- package/src/agents/reviewer/AGENT.md +4 -2
- package/src/agents/reviewer/agent.json +1 -1
- package/src/defaults/shipped-history.json +10 -0
- package/src/defaults/skills/browser-use/SKILL.md +5 -0
- package/src/defaults/skills/memory-management/SKILL.md +4 -1
- package/src/defaults/skills/setup/references/actions.md +14 -4
- package/src/defaults/skills/setup-guide/SKILL.md +178 -0
- package/src/defaults/skills/setup-guide/model-recommendations.json +333 -0
- package/src/lib/rules-builder.cjs +55 -6
- package/src/output-styles/common.md +13 -8
- package/src/rules/agent/AGENT.md +4 -0
- package/src/rules/agent-mode/AGENT_MODE.md +24 -0
- package/src/rules/agent-mode/MIXY.md +28 -0
- package/src/rules/agent-mode/ROUTINE.md +6 -0
- package/src/rules/lead/LEAD.md +7 -3
- package/src/rules/shared/70-delivery.md +3 -0
- package/src/runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs +86 -0
- package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +77 -14
- package/src/runtime/agent/orchestrator/context/role-instructions.mjs +241 -14
- package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +4 -2
- package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +216 -0
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +5 -4
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +10 -1
- package/src/runtime/agent/orchestrator/mcp/client.mjs +115 -11
- package/src/runtime/agent/orchestrator/mcp/oauth-account.mjs +97 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +49 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +59 -9
- package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +32 -7
- package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +45 -6
- package/src/runtime/agent/orchestrator/mcp/oauth.mjs +195 -6
- package/src/runtime/agent/orchestrator/mcp/server-connectors.mjs +17 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +7 -2
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +10 -3
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/auto-effort.mjs +7 -8
- package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/lib/tool-call-wire-ids.mjs +65 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +8 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +9 -2
- package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +17 -12
- package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +10 -0
- package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +14 -2
- package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +17 -0
- package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +96 -1
- package/src/runtime/agent/orchestrator/runtime-core/context-status-shape.mjs +4 -0
- package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +6 -4
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +14 -0
- package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +1 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-bridge.mjs +12 -6
- package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +32 -5
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +21 -5
- package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +40 -7
- package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +52 -20
- package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/agent-home.d.mts +4 -0
- package/src/runtime/agent/orchestrator/session/agent-home.mjs +33 -0
- package/src/runtime/agent/orchestrator/session/agent-mode.mjs +62 -0
- package/src/runtime/agent/orchestrator/session/agent-room-supervisor.d.mts +31 -0
- package/src/runtime/agent/orchestrator/session/agent-room-supervisor.mjs +241 -0
- package/src/runtime/agent/orchestrator/session/agent-room.d.mts +166 -0
- package/src/runtime/agent/orchestrator/session/agent-room.mjs +650 -0
- package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +7 -1
- package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +6 -2
- package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +192 -0
- package/src/runtime/agent/orchestrator/session/human-gate/held-message.mjs +95 -0
- package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +221 -0
- package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +195 -0
- package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +167 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/loop/loop-state.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +10 -0
- package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +108 -6
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +10 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +74 -15
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +9 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +5 -4
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +13 -3
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-commit.mjs +18 -8
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-lifecycle-epoch.mjs +0 -8
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +44 -16
- package/src/runtime/agent/orchestrator/session/manager/provider-replay.mjs +50 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +22 -5
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +39 -4
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +33 -24
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +58 -8
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +3 -2
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +7 -1
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/manager/workflow-switch-notice.mjs +58 -0
- package/src/runtime/agent/orchestrator/session/mixy-name.mjs +27 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +32 -5
- package/src/runtime/agent/orchestrator/session/routine-result.d.mts +86 -0
- package/src/runtime/agent/orchestrator/session/routine-result.mjs +329 -0
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/store-record-strict-scan.mjs +28 -9
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +16 -2
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +44 -12
- package/src/runtime/agent/orchestrator/session/store-summary-last-message.mjs +46 -0
- package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +0 -4
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +58 -10
- package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +92 -15
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +113 -5
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +4 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +25 -1
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +6 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +3 -0
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +75 -57
- package/src/runtime/attachments/store.mjs +6 -0
- package/src/runtime/browser-bridge/client.mjs +24 -0
- package/src/runtime/channels/lib/scheduler.mjs +40 -12
- package/src/runtime/channels/lib/schedules-db.mjs +71 -12
- package/src/runtime/channels/lib/webhook/session-dispatch.mjs +36 -16
- package/src/runtime/channels/lib/webhook.mjs +0 -1
- package/src/runtime/channels/lib/webhooks-db.mjs +100 -10
- package/src/runtime/channels/lib/worker-main.mjs +17 -20
- package/src/runtime/computer-bridge/client.mjs +28 -1
- package/src/runtime/connectors/gmail-app-password.mjs +690 -0
- package/src/runtime/connectors/google-mcp.mjs +446 -0
- package/src/runtime/effort-judge/judge-client.mjs +1 -1
- package/src/runtime/effort-judge/model-manifest.json +7 -7
- package/src/runtime/memory/lib/core-memory-access.mjs +69 -0
- package/src/runtime/memory/lib/core-memory-file.mjs +16 -8
- package/src/runtime/memory/lib/core-memory-index.mjs +15 -9
- package/src/runtime/memory/lib/core-memory-management.mjs +10 -4
- package/src/runtime/memory/lib/core-memory-store.mjs +53 -30
- package/src/runtime/memory/lib/core-memory-uniqueness.mjs +10 -6
- package/src/runtime/memory/lib/http-router/session-routes.mjs +3 -2
- package/src/runtime/memory/lib/memory-action-handlers/core-actions.mjs +44 -14
- package/src/runtime/memory/lib/memory-schema/core-and-meta.mjs +11 -0
- package/src/runtime/memory/lib/memory.mjs +7 -1
- package/src/runtime/memory/lib/query-core-recall.mjs +9 -1
- package/src/runtime/memory/lib/query-hybrid-search.mjs +2 -0
- package/src/runtime/memory/lib/tool-call-handler.mjs +2 -0
- package/src/runtime/memory/tool-defs.mjs +5 -0
- package/src/runtime/shared/agent-avatar-sprites.mjs +445 -0
- package/src/runtime/shared/agent-avatar.d.mts +64 -0
- package/src/runtime/shared/agent-avatar.mjs +320 -0
- package/src/runtime/shared/agent-kind.mjs +152 -0
- package/src/runtime/shared/agent-route-config.d.mts +4 -0
- package/src/runtime/shared/agent-route-config.mjs +30 -0
- package/src/runtime/shared/agent-store.mjs +132 -0
- package/src/runtime/shared/atomic-file.mjs +14 -1
- package/src/runtime/shared/bot-split-migration.mjs +457 -0
- package/src/runtime/shared/config-patch.mjs +20 -1
- package/src/runtime/shared/config.mjs +8 -1
- package/src/runtime/shared/connector-accounts.mjs +15 -0
- package/src/runtime/shared/connector-catalog.d.mts +51 -0
- package/src/runtime/shared/connector-catalog.mjs +228 -0
- package/src/runtime/shared/connector-triggers.d.mts +3 -0
- package/src/runtime/shared/connector-triggers.mjs +45 -0
- package/src/runtime/shared/err-text.mjs +3 -1
- package/src/runtime/shared/error-presentation.mjs +91 -6
- package/src/runtime/shared/google-oauth-client.mjs +24 -0
- package/src/runtime/shared/handoff-artifacts.d.mts +20 -0
- package/src/runtime/shared/handoff-artifacts.mjs +61 -0
- package/src/runtime/shared/held-request-host.d.mts +12 -0
- package/src/runtime/shared/held-request-host.mjs +71 -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/model-route-fallback.mjs +79 -7
- package/src/runtime/shared/plain-preview.d.mts +1 -0
- package/src/runtime/shared/plain-preview.mjs +33 -0
- package/src/runtime/shared/routine-limits.d.mts +11 -0
- package/src/runtime/shared/routine-limits.mjs +21 -0
- package/src/runtime/shared/routine-trigger.d.mts +10 -0
- package/src/runtime/shared/routine-trigger.mjs +44 -0
- package/src/runtime/shared/schedule-model-ref.mjs +9 -10
- 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/slow-span.mjs +4 -12
- package/src/runtime/shared/statusline/statusline-segments.mjs +13 -1
- package/src/runtime/shared/transcript-metadata.mjs +10 -0
- package/src/runtime/shared/turn-changes.mjs +0 -8
- package/src/runtime/shared/usage-readout-totals.mjs +58 -0
- package/src/runtime/shared/user-data-guard.mjs +119 -38
- package/src/session-runtime/agent-home-delivery.mjs +826 -0
- package/src/session-runtime/agent-profile-changes.mjs +21 -0
- package/src/session-runtime/agent-room-delivery.mjs +133 -0
- package/src/session-runtime/agent-task-sessions.mjs +460 -0
- package/src/session-runtime/boot/apis.mjs +63 -18
- package/src/session-runtime/boot/facade.mjs +5 -8
- package/src/session-runtime/boot/infrastructure.mjs +28 -17
- package/src/session-runtime/boot/providers.mjs +14 -4
- package/src/session-runtime/boot/tools.mjs +3 -0
- package/src/session-runtime/channel-config-api.mjs +53 -5
- package/src/session-runtime/config-lifecycle.mjs +43 -2
- package/src/session-runtime/cwd-plugins/core-memory-context.mjs +36 -20
- package/src/session-runtime/cwd-plugins/plugin-status.mjs +6 -1
- package/src/session-runtime/handoff-attachments.mjs +593 -0
- package/src/session-runtime/held-requests.mjs +228 -0
- package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +121 -0
- package/src/session-runtime/internal-tool-executor/feature-tools.mjs +5 -1
- package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +31 -2
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +56 -3
- package/src/session-runtime/internal-tool-executor.mjs +2 -0
- package/src/session-runtime/lifecycle/session-catalog.mjs +9 -0
- package/src/session-runtime/lifecycle/session-switch/resume.mjs +4 -0
- package/src/session-runtime/lifecycle/shared.mjs +6 -0
- package/src/session-runtime/mcp-glue/server-catalog.mjs +8 -0
- package/src/session-runtime/mcp-glue/server-input.mjs +3 -0
- package/src/session-runtime/mcp-oauth-login.mjs +112 -15
- package/src/session-runtime/model-route/route-persist.mjs +2 -2
- package/src/session-runtime/model-route/route-tuning.mjs +7 -1
- package/src/session-runtime/model-route/set-route.mjs +35 -4
- 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 +229 -0
- package/src/session-runtime/resource-api.mjs +5 -0
- package/src/session-runtime/resource-mcp-api.mjs +107 -13
- package/src/session-runtime/resource-skills-api.mjs +12 -0
- package/src/session-runtime/room-member-gate.mjs +48 -0
- package/src/session-runtime/route-resolution.mjs +5 -11
- package/src/session-runtime/routine-home-turn.mjs +156 -0
- package/src/session-runtime/routine-owner.mjs +48 -0
- package/src/session-runtime/schedule-session-run.mjs +30 -64
- package/src/session-runtime/services/agent-tool/helpers.mjs +30 -1
- package/src/session-runtime/services/agent-tool/send-flow.mjs +11 -3
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +19 -4
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +2 -0
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +4 -1
- 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/channel-admin.mjs +93 -24
- package/src/session-runtime/services/memory-access.mjs +37 -0
- package/src/session-runtime/services/provider-admin.mjs +74 -39
- package/src/session-runtime/services/schedule-access.mjs +91 -0
- package/src/session-runtime/session-create/session-options.mjs +34 -7
- package/src/session-runtime/session-hooks.mjs +16 -8
- package/src/session-runtime/setup-tool/executor.mjs +79 -6
- package/src/session-runtime/setup-tool/extended-actions.mjs +90 -20
- package/src/session-runtime/setup-tool/settings-contract.mjs +14 -6
- package/src/session-runtime/setup-tool/tool-defs.mjs +8 -0
- package/src/session-runtime/skills-api.mjs +16 -1
- package/src/session-runtime/tool-policy-refresh.mjs +249 -29
- package/src/session-runtime/turn/session-ops.mjs +8 -1
- package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +10 -2
- package/src/session-runtime/turn/turn-run/turn-prepare.mjs +14 -0
- package/src/session-runtime/webhook-session-run.mjs +49 -67
- package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +239 -0
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +650 -25
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +67 -7
- package/src/session-runtime/workflow-agents-api/onboarding.mjs +11 -4
- package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +11 -1
- package/src/session-runtime/workflow-agents-api.mjs +4 -1
- package/src/standalone/daemon-agent-control.mjs +1 -8
- package/src/standalone/daemon-session-control.mjs +21 -4
- package/src/standalone/daemon.mjs +31 -5
- package/src/standalone/session-protocol.mjs +27 -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 +28 -0
- package/src/standalone/session-service.mjs +19 -0
- package/src/tui/app/slash-commands.mjs +20 -0
- package/src/tui/dist/index.mjs +234 -95
- package/src/tui/session/context-state.mjs +6 -1
- package/src/tui/session/notice-surface.mjs +34 -4
- package/src/tui/session/queue-helpers.mjs +2 -1
- package/src/tui/session/session-api/extensions.mjs +4 -0
- package/src/tui/session/session-api/intake/submission.mjs +5 -0
- package/src/tui/session/session-api/integrations.mjs +8 -0
- package/src/tui/session/session-api/lifecycle/session-reset.mjs +13 -3
- package/src/tui/session/session-api/lifecycle.mjs +17 -1
- package/src/tui/session/session-api/routes.mjs +61 -5
- package/src/tui/session/session-flow/drain.mjs +12 -3
- package/src/tui/session/session-flow/queue/entry-shape.mjs +2 -1
- package/src/tui/session/session-flow/queue/take-entries.mjs +3 -2
- package/src/tui/session/session-flow/steering.mjs +58 -24
- package/src/tui/session/tool-approval.mjs +26 -5
- package/src/tui/session/transcript-intake.mjs +8 -2
- package/src/tui/session/transcript-spill.mjs +5 -1
- package/src/tui/session/tui-steering-persist.mjs +65 -42
- package/src/tui/session/turn-live-call.mjs +64 -0
- package/src/tui/session/turn.mjs +34 -8
- package/src/session-runtime/automation-agents.mjs +0 -94
- package/src/session-runtime/automation-workflow.mjs +0 -59
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attachments and artifacts handed between bots and agents — one mechanism
|
|
3
|
+
* for every direction: a bot or agent forwarding files to another session
|
|
4
|
+
* (`session` send/create, the `agent` tool, group-room speech) and a target
|
|
5
|
+
* turn's produced files coming back with its report (task-session reports,
|
|
6
|
+
* agent completions, room speech).
|
|
7
|
+
*
|
|
8
|
+
* Forward: `resolveHandoffAttachments` turns `attachments` references — an
|
|
9
|
+
* attachment id this conversation already shows (a user's upload, a report's
|
|
10
|
+
* artifact) or a file path inside the caller's own folders — into the stored
|
|
11
|
+
* reference parts a desktop prompt carries (attachments/store.mjs), so the
|
|
12
|
+
* target turn receives real images and files under the composer's limits.
|
|
13
|
+
*
|
|
14
|
+
* Return: `collectTurnArtifacts` gathers the files a finished turn produced
|
|
15
|
+
* (its edits and patches, media/office/visualize outputs, and every file its
|
|
16
|
+
* final reply names), keeps those inside the target's folders, stores the
|
|
17
|
+
* attachable ones and lists the rest as paths.
|
|
18
|
+
*
|
|
19
|
+
* Every path is resolved through its links and must lie inside an allowed
|
|
20
|
+
* root; credential/secret files (runtime/shared/sensitive-path-rules.mjs) are
|
|
21
|
+
* never attached.
|
|
22
|
+
*/
|
|
23
|
+
import { closeSync, fstatSync, lstatSync, openSync, readSync, realpathSync } from 'node:fs';
|
|
24
|
+
import { basename, dirname, extname, isAbsolute, resolve, sep } from 'node:path';
|
|
25
|
+
import { readAttachmentBuffer, storeAttachmentBuffer } from '../runtime/attachments/store.mjs';
|
|
26
|
+
import { SUPPORTED_IMAGE_MIME_TYPES, isPdfBuffer } from '../runtime/attachments/limits.mjs';
|
|
27
|
+
import { ROOM_ROW_SOURCE } from '../runtime/agent/orchestrator/session/agent-room.mjs';
|
|
28
|
+
import { isSensitiveLocalPath } from '../runtime/shared/sensitive-path-rules.mjs';
|
|
29
|
+
import { normalizeMessageAttachments } from '../runtime/shared/transcript-metadata.mjs';
|
|
30
|
+
import { resolvePluginData } from '../runtime/shared/plugin-paths.mjs';
|
|
31
|
+
|
|
32
|
+
/** The desktop composer's limits (apps/desktop/src/shared/prompt-limits.ts,
|
|
33
|
+
* renderer/composer-support.tsx), in bytes. */
|
|
34
|
+
export const HANDOFF_LIMITS = Object.freeze({
|
|
35
|
+
count: 8,
|
|
36
|
+
imageBytes: 12_000_000,
|
|
37
|
+
imageTotal: 22_500_000,
|
|
38
|
+
documentBytes: 20 * 1024 * 1024,
|
|
39
|
+
documentTotal: 21_000_000,
|
|
40
|
+
textBytes: 750_000,
|
|
41
|
+
textTotal: 850_000,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const ATTACHMENT_ID = /^[a-f0-9]{64}$/;
|
|
45
|
+
const IMAGE_TYPES = {
|
|
46
|
+
'.png': 'image/png',
|
|
47
|
+
'.jpg': 'image/jpeg',
|
|
48
|
+
'.jpeg': 'image/jpeg',
|
|
49
|
+
'.gif': 'image/gif',
|
|
50
|
+
'.webp': 'image/webp',
|
|
51
|
+
};
|
|
52
|
+
const DOCUMENT_TYPES = {
|
|
53
|
+
'.pdf': 'application/pdf',
|
|
54
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
55
|
+
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
56
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
57
|
+
'.xlsm': 'application/vnd.ms-excel.sheet.macroEnabled.12',
|
|
58
|
+
'.doc': 'application/msword',
|
|
59
|
+
'.xls': 'application/vnd.ms-excel',
|
|
60
|
+
'.ppt': 'application/vnd.ms-powerpoint',
|
|
61
|
+
};
|
|
62
|
+
const TEXT_TYPES = {
|
|
63
|
+
'.md': 'text/markdown',
|
|
64
|
+
'.html': 'text/html',
|
|
65
|
+
'.htm': 'text/html',
|
|
66
|
+
'.csv': 'text/csv',
|
|
67
|
+
'.json': 'application/json',
|
|
68
|
+
'.svg': 'image/svg+xml',
|
|
69
|
+
};
|
|
70
|
+
const TEXT_EXTENSIONS = new Set([
|
|
71
|
+
...Object.keys(TEXT_TYPES),
|
|
72
|
+
...'.mdx .txt .log .jsonl .yaml .yml .toml .xml .tsv .js .jsx .mjs .cjs .ts .tsx .mts .cts .py .rb .rs .go .java .kt .swift .cs .cpp .cc .c .h .hh .hpp .sh .zsh .ps1 .bat .cmd .sql .css .scss .sass .vue .svelte .ini .conf .cfg .gql .graphql'.split(
|
|
73
|
+
' '
|
|
74
|
+
),
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
const samePlatformPath = (path) => (process.platform === 'win32' ? path.toLowerCase() : path);
|
|
78
|
+
|
|
79
|
+
function realOrNull(path) {
|
|
80
|
+
try {
|
|
81
|
+
return realpathSync.native(path);
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The folders a session may hand files from and report artifacts in: its
|
|
88
|
+
* working directory and Project (worktree) roots, through their links. A
|
|
89
|
+
* filesystem root never counts. */
|
|
90
|
+
export function sessionRoots(session, cwd = '') {
|
|
91
|
+
const candidates = [
|
|
92
|
+
cwd,
|
|
93
|
+
session?.cwd,
|
|
94
|
+
session?.workspace?.projectPath,
|
|
95
|
+
session?.workspace?.worktreePath,
|
|
96
|
+
session?.desktopSession?.projectPath,
|
|
97
|
+
];
|
|
98
|
+
const roots = [];
|
|
99
|
+
for (const candidate of candidates) {
|
|
100
|
+
if (typeof candidate !== 'string' || !candidate.trim()) continue;
|
|
101
|
+
const real = realOrNull(resolve(candidate));
|
|
102
|
+
if (!real || dirname(real) === real) continue;
|
|
103
|
+
if (!roots.some((root) => samePlatformPath(root) === samePlatformPath(real))) roots.push(real);
|
|
104
|
+
}
|
|
105
|
+
return roots;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function within(root, path) {
|
|
109
|
+
const base = samePlatformPath(root);
|
|
110
|
+
const target = samePlatformPath(path);
|
|
111
|
+
return target === base || target.startsWith(base.endsWith(sep) ? base : `${base}${sep}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** `{ kind: 'image' | 'file' | 'text', mimeType }` of a file name and its first
|
|
115
|
+
* bytes; null when it is none of the types a prompt can carry. */
|
|
116
|
+
function classify(name, head) {
|
|
117
|
+
const ext = extname(name).toLowerCase();
|
|
118
|
+
if (IMAGE_TYPES[ext]) return { kind: 'image', mimeType: IMAGE_TYPES[ext] };
|
|
119
|
+
if (DOCUMENT_TYPES[ext] || isPdfBuffer(head)) {
|
|
120
|
+
return { kind: 'file', mimeType: isPdfBuffer(head) ? 'application/pdf' : DOCUMENT_TYPES[ext] };
|
|
121
|
+
}
|
|
122
|
+
if (TEXT_EXTENSIONS.has(ext) || (!ext && head.length && !head.includes(0))) {
|
|
123
|
+
return { kind: 'text', mimeType: TEXT_TYPES[ext] || 'text/plain' };
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The limit `kind` keeps per file and in total. */
|
|
129
|
+
function limitsOf(kind) {
|
|
130
|
+
if (kind === 'image') return { each: HANDOFF_LIMITS.imageBytes, total: HANDOFF_LIMITS.imageTotal, key: 'image' };
|
|
131
|
+
if (kind === 'text') return { each: HANDOFF_LIMITS.textBytes, total: HANDOFF_LIMITS.textTotal, key: 'text' };
|
|
132
|
+
return { each: HANDOFF_LIMITS.documentBytes, total: HANDOFF_LIMITS.documentTotal, key: 'file' };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** A stored reference part of one attachment, as a desktop prompt carries it. */
|
|
136
|
+
function referencePart({ kind, attachmentRef, mimeType, name, size }) {
|
|
137
|
+
const common = { attachmentRef, sizeBytes: size, mimeType, filename: name };
|
|
138
|
+
if (kind === 'image') return { type: 'image', ...common };
|
|
139
|
+
if (kind === 'text') return { type: 'text', textKind: 'file', ...common };
|
|
140
|
+
return { type: 'file', ...common };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Only the reference fields of `parts` (never inline bytes or paths): the
|
|
144
|
+
* form a forwarded prompt may carry, whoever built the parts. */
|
|
145
|
+
export function sanitizeHandoffParts(parts) {
|
|
146
|
+
const out = [];
|
|
147
|
+
for (const part of Array.isArray(parts) ? parts : []) {
|
|
148
|
+
if (out.length >= HANDOFF_LIMITS.count) break;
|
|
149
|
+
if (!part || !['image', 'file', 'text'].includes(part.type)) continue;
|
|
150
|
+
if (!ATTACHMENT_ID.test(String(part.attachmentRef ?? ''))) continue;
|
|
151
|
+
out.push(
|
|
152
|
+
referencePart({
|
|
153
|
+
kind: part.type,
|
|
154
|
+
attachmentRef: part.attachmentRef,
|
|
155
|
+
mimeType: String(part.mimeType || (part.type === 'image' ? 'image/png' : 'application/octet-stream')).slice(
|
|
156
|
+
0,
|
|
157
|
+
100
|
|
158
|
+
),
|
|
159
|
+
name: String(part.filename || 'attachment').slice(0, 200),
|
|
160
|
+
size: Number.isFinite(part.sizeBytes) && part.sizeBytes >= 0 ? part.sizeBytes : 0,
|
|
161
|
+
})
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The prompt a forwarded turn submits: plain text, or text plus the
|
|
168
|
+
* attachment parts. */
|
|
169
|
+
export function handoffContent(text, parts) {
|
|
170
|
+
const clean = sanitizeHandoffParts(parts);
|
|
171
|
+
const body = String(text ?? '');
|
|
172
|
+
return clean.length ? [{ type: 'text', text: body }, ...clean] : body;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const textOf = (content) =>
|
|
176
|
+
typeof content === 'string'
|
|
177
|
+
? content
|
|
178
|
+
: (Array.isArray(content) ? content : [])
|
|
179
|
+
.map((part) => (typeof part === 'string' ? part : typeof part?.text === 'string' ? part.text : ''))
|
|
180
|
+
.join('\n');
|
|
181
|
+
|
|
182
|
+
/** The attachments a conversation already shows, by id, newest last: only
|
|
183
|
+
* what the runtime itself recorded in structured fields — a user message's
|
|
184
|
+
* stored reference parts and attachment records (intake, a forwarded
|
|
185
|
+
* turn's authorized parts, a task report's returned files) and a room
|
|
186
|
+
* speech row's stored files. Text never names one: a model can print any
|
|
187
|
+
* id. Only these may be forwarded by id or name. */
|
|
188
|
+
export function conversationAttachments(messages) {
|
|
189
|
+
const found = new Map();
|
|
190
|
+
const add = (id, info) => {
|
|
191
|
+
if (!ATTACHMENT_ID.test(String(id || ''))) return;
|
|
192
|
+
const kind = ['image', 'file', 'text'].includes(info.kind) ? info.kind : 'file';
|
|
193
|
+
// A later occurrence moves the id to the end: iteration order is recency.
|
|
194
|
+
found.delete(id);
|
|
195
|
+
found.set(id, {
|
|
196
|
+
id,
|
|
197
|
+
kind,
|
|
198
|
+
name: String(info.name || 'attachment').slice(0, 200),
|
|
199
|
+
mimeType: String(info.mimeType || (kind === 'image' ? 'image/png' : 'application/octet-stream')).slice(0, 100),
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
const partInfo = (part) => ({
|
|
203
|
+
kind: part.type === 'image' ? 'image' : part.type === 'text' ? 'text' : 'file',
|
|
204
|
+
name: part.filename || part.name,
|
|
205
|
+
mimeType: part.mimeType || part.mediaType,
|
|
206
|
+
});
|
|
207
|
+
for (const message of Array.isArray(messages) ? messages : []) {
|
|
208
|
+
const user = message?.role === 'user';
|
|
209
|
+
const speech = message?.meta?.source === ROOM_ROW_SOURCE && message.meta.room?.kind === 'speech';
|
|
210
|
+
// Assistant and tool messages hold what a model or tool wrote: never trusted.
|
|
211
|
+
if (!user && !speech) continue;
|
|
212
|
+
const parts = [
|
|
213
|
+
...(user && Array.isArray(message.content) ? message.content : []),
|
|
214
|
+
...(Array.isArray(message.meta?.room?.parts) ? message.meta.room.parts : []),
|
|
215
|
+
];
|
|
216
|
+
for (const part of parts) {
|
|
217
|
+
if (!part || typeof part !== 'object' || !['image', 'file', 'text'].includes(part.type)) continue;
|
|
218
|
+
if (!part.attachmentRef || (part.type === 'text' && !part.textKind)) continue;
|
|
219
|
+
add(part.attachmentRef, partInfo(part));
|
|
220
|
+
}
|
|
221
|
+
for (const record of normalizeMessageAttachments(message.meta?.transcript?.attachments)) add(record.id, record);
|
|
222
|
+
}
|
|
223
|
+
return found;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function handoffError(surface, message) {
|
|
227
|
+
return new Error(`${surface}: ${message}`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Whether the canonical path `real` names exactly the file `opened` (the
|
|
231
|
+
* fstat of an open handle): every directory on the way is a real directory,
|
|
232
|
+
* never a link, and the final entry is that same file (device, file id and
|
|
233
|
+
* birth time). Node has no portable path-of-handle, so a link or file
|
|
234
|
+
* swapped in after the open fails this instead. */
|
|
235
|
+
function samePathAndHandle(real, opened) {
|
|
236
|
+
const chain = [];
|
|
237
|
+
for (let current = real; dirname(current) !== current; current = dirname(current)) chain.unshift(current);
|
|
238
|
+
for (let index = 0; index < chain.length; index += 1) {
|
|
239
|
+
let info;
|
|
240
|
+
try {
|
|
241
|
+
info = lstatSync(chain[index], { bigint: true });
|
|
242
|
+
} catch {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
if (info.isSymbolicLink()) return false;
|
|
246
|
+
const last = index === chain.length - 1;
|
|
247
|
+
if (!last && !info.isDirectory()) return false;
|
|
248
|
+
if (last) {
|
|
249
|
+
return (
|
|
250
|
+
info.isFile() && info.dev === opened.dev && info.ino === opened.ino && info.birthtimeNs === opened.birthtimeNs
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Open `raw` (absolute, or relative to `cwd`) once and validate the OPEN
|
|
259
|
+
* handle: a regular file whose canonical path lies inside `roots`, is no
|
|
260
|
+
* credential/secret file, and still names that very handle. Returns `{ fd,
|
|
261
|
+
* path, size }` (the caller closes `fd` and reads only through it); throws
|
|
262
|
+
* with the reason otherwise, the handle closed.
|
|
263
|
+
*/
|
|
264
|
+
function openContained(raw, { cwd, roots, surface }) {
|
|
265
|
+
const value = String(raw ?? '').trim();
|
|
266
|
+
if (!value || value.includes('\0')) throw handoffError(surface, 'an attachment path is empty or invalid');
|
|
267
|
+
const absolute = isAbsolute(value) ? resolve(value) : resolve(cwd || process.cwd(), value);
|
|
268
|
+
let fd;
|
|
269
|
+
try {
|
|
270
|
+
fd = openSync(absolute, 'r');
|
|
271
|
+
} catch {
|
|
272
|
+
throw handoffError(surface, `attachment "${value}" does not exist`);
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
const opened = fstatSync(fd, { bigint: true });
|
|
276
|
+
if (!opened.isFile()) throw handoffError(surface, `attachment "${value}" is not a file`);
|
|
277
|
+
const real = realOrNull(absolute);
|
|
278
|
+
if (!real) throw handoffError(surface, `attachment "${value}" does not exist`);
|
|
279
|
+
if (!roots.some((root) => within(root, real))) {
|
|
280
|
+
throw handoffError(surface, `attachment "${value}" is outside this session's folders`);
|
|
281
|
+
}
|
|
282
|
+
const extraData = [resolvePluginData()];
|
|
283
|
+
if (isSensitiveLocalPath(absolute, { extraData }) || isSensitiveLocalPath(real, { extraData })) {
|
|
284
|
+
throw handoffError(surface, `attachment "${value}" can hold credentials or secrets and is never attached`);
|
|
285
|
+
}
|
|
286
|
+
if (!samePathAndHandle(real, opened)) {
|
|
287
|
+
throw handoffError(surface, `attachment "${value}" changed while it was opened`);
|
|
288
|
+
}
|
|
289
|
+
return { fd, path: real, size: Number(opened.size), opened, value };
|
|
290
|
+
} catch (error) {
|
|
291
|
+
closeSync(fd);
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** The first `length` bytes of `fd`. */
|
|
297
|
+
function readHead(fd, length = 4096) {
|
|
298
|
+
const buffer = Buffer.alloc(length);
|
|
299
|
+
const read = readSync(fd, buffer, 0, length, 0);
|
|
300
|
+
return buffer.subarray(0, read);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** All bytes of `fd` when there are at most `cap`, else null: at most
|
|
304
|
+
* `cap + 1` bytes are ever read, whatever the file claims or grows to. */
|
|
305
|
+
function readCapped(fd, cap) {
|
|
306
|
+
const chunks = [];
|
|
307
|
+
let total = 0;
|
|
308
|
+
const chunk = Buffer.alloc(Math.min(cap + 1, 1 << 20));
|
|
309
|
+
for (;;) {
|
|
310
|
+
const read = readSync(fd, chunk, 0, Math.min(chunk.length, cap + 1 - total), total);
|
|
311
|
+
if (read === 0) break;
|
|
312
|
+
chunks.push(Buffer.from(chunk.subarray(0, read)));
|
|
313
|
+
total += read;
|
|
314
|
+
if (total > cap) return null;
|
|
315
|
+
}
|
|
316
|
+
return Buffer.concat(chunks, total);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** The handle checks, for tests. */
|
|
320
|
+
export const handoffFileChecks = Object.freeze({
|
|
321
|
+
samePathAndHandle,
|
|
322
|
+
readCapped,
|
|
323
|
+
// `afterRead(path)` runs between the read and the re-check (a test's swap).
|
|
324
|
+
readContained: (raw, options) => readContained(raw, options),
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
/** `{ path, name, type, bytes, size }` of one contained file, read once
|
|
328
|
+
* through its validated handle: `bytes` null when it is over its kind's
|
|
329
|
+
* per-file limit or of no type a prompt carries. */
|
|
330
|
+
function readContained(raw, options) {
|
|
331
|
+
const file = openContained(raw, options);
|
|
332
|
+
try {
|
|
333
|
+
const name = basename(file.path);
|
|
334
|
+
const type = classify(name, readHead(file.fd));
|
|
335
|
+
const bytes = type ? readCapped(file.fd, limitsOf(type.kind).each) : null;
|
|
336
|
+
options.afterRead?.(file.path);
|
|
337
|
+
// The same checks again after the read: a path re-pointed (a link or
|
|
338
|
+
// file swapped in) while it was read is rejected. Residual window: a swap
|
|
339
|
+
// made and undone between two checks still passes. Accepted: an agent
|
|
340
|
+
// that can do that already has a shell, so this is no hard boundary.
|
|
341
|
+
if (!samePathAndHandle(file.path, file.opened)) {
|
|
342
|
+
throw handoffError(options.surface, `attachment "${file.value}" changed while it was read`);
|
|
343
|
+
}
|
|
344
|
+
return { path: file.path, name, type, bytes, size: bytes ? bytes.length : file.size };
|
|
345
|
+
} finally {
|
|
346
|
+
closeSync(file.fd);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Running totals per kind against HANDOFF_LIMITS. */
|
|
351
|
+
function createBudget() {
|
|
352
|
+
const totals = { image: 0, text: 0, file: 0 };
|
|
353
|
+
return {
|
|
354
|
+
/** Whether `size` bytes of `kind` fit; counts them when they do. */
|
|
355
|
+
take(kind, size) {
|
|
356
|
+
const { each, total, key } = limitsOf(kind);
|
|
357
|
+
if (size > each || totals[key] + size > total) return false;
|
|
358
|
+
totals[key] += size;
|
|
359
|
+
return true;
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* The prompt parts for `refs` (the tool's `attachments`): each an attachment
|
|
366
|
+
* this conversation shows (`messages`), by its id or its file name (the
|
|
367
|
+
* newest of that name), or a file path inside `roots`.
|
|
368
|
+
* Returns `{ parts, records }`; throws on an unknown id, a path outside the
|
|
369
|
+
* roots, a secret file, an unsupported type or a limit, naming the reference.
|
|
370
|
+
*/
|
|
371
|
+
export function resolveHandoffAttachments(refs, { messages = [], roots = [], cwd = '', surface = 'session' } = {}) {
|
|
372
|
+
if (refs == null) return { parts: [], records: [] };
|
|
373
|
+
if (!Array.isArray(refs) || refs.some((ref) => typeof ref !== 'string')) {
|
|
374
|
+
throw handoffError(surface, 'attachments must be a list of attachment ids or file paths');
|
|
375
|
+
}
|
|
376
|
+
const list = [...new Set(refs.map((ref) => ref.trim()).filter(Boolean))];
|
|
377
|
+
if (list.length > HANDOFF_LIMITS.count) {
|
|
378
|
+
throw handoffError(surface, `at most ${HANDOFF_LIMITS.count} attachments can be passed at once`);
|
|
379
|
+
}
|
|
380
|
+
const known = conversationAttachments(messages);
|
|
381
|
+
// An upload the model knows only by the name its prompt showed
|
|
382
|
+
// (`[Image: source: photo.png …]`): the newest attachment of that name.
|
|
383
|
+
const named = (ref) => [...known.values()].findLast((record) => record.name === ref) || null;
|
|
384
|
+
const budget = createBudget();
|
|
385
|
+
const parts = [];
|
|
386
|
+
const records = [];
|
|
387
|
+
for (const ref of list) {
|
|
388
|
+
let entry;
|
|
389
|
+
const record = ATTACHMENT_ID.test(ref) ? known.get(ref) : named(ref);
|
|
390
|
+
if (ATTACHMENT_ID.test(ref) && !record)
|
|
391
|
+
throw handoffError(surface, `attachment ${ref} is not in this conversation`);
|
|
392
|
+
if (record) {
|
|
393
|
+
let bytes;
|
|
394
|
+
try {
|
|
395
|
+
bytes = readAttachmentBuffer(record.id);
|
|
396
|
+
} catch {
|
|
397
|
+
throw handoffError(surface, `attachment "${ref}" is no longer stored`);
|
|
398
|
+
}
|
|
399
|
+
entry = { ...record, attachmentRef: record.id, size: bytes.length };
|
|
400
|
+
} else {
|
|
401
|
+
const { name, type, bytes } = readContained(ref, { cwd, roots, surface });
|
|
402
|
+
if (!type) throw handoffError(surface, `attachment "${ref}" is not an image, PDF, Office or text file`);
|
|
403
|
+
if (!bytes) throw handoffError(surface, `attachment "${ref}" is larger than the attachment limit`);
|
|
404
|
+
if (type.kind === 'image' && !SUPPORTED_IMAGE_MIME_TYPES.has(type.mimeType)) {
|
|
405
|
+
throw handoffError(surface, `attachment "${ref}" is not a PNG, JPEG, GIF or WebP image`);
|
|
406
|
+
}
|
|
407
|
+
entry = { ...type, name, size: bytes.length, attachmentRef: storeAttachmentBuffer(bytes).attachmentRef };
|
|
408
|
+
}
|
|
409
|
+
if (!budget.take(entry.kind, entry.size)) {
|
|
410
|
+
throw handoffError(surface, `attachment "${entry.name}" exceeds the attachment size limits`);
|
|
411
|
+
}
|
|
412
|
+
parts.push(referencePart(entry));
|
|
413
|
+
records.push({
|
|
414
|
+
id: entry.attachmentRef,
|
|
415
|
+
attachmentRef: entry.attachmentRef,
|
|
416
|
+
kind: entry.kind,
|
|
417
|
+
name: entry.name,
|
|
418
|
+
mimeType: entry.mimeType,
|
|
419
|
+
size: entry.size,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
return { parts, records };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ── Return path: what a finished turn produced ────────────────────────────
|
|
426
|
+
|
|
427
|
+
const OFFICE_WRITE_ACTIONS = new Set([
|
|
428
|
+
'author',
|
|
429
|
+
'create',
|
|
430
|
+
'batch',
|
|
431
|
+
'render',
|
|
432
|
+
'save',
|
|
433
|
+
'finalize',
|
|
434
|
+
'secure',
|
|
435
|
+
'commit',
|
|
436
|
+
'refine',
|
|
437
|
+
'design',
|
|
438
|
+
]);
|
|
439
|
+
const PATCH_FILE = /^\*\*\* (?:Add File|Update File|Move to): (.+)$/gm;
|
|
440
|
+
const MARKDOWN_LINK = /\]\(\s*<?([^)\s>]+)>?\s*\)/g;
|
|
441
|
+
const BACKTICK_PATH = /`([^`\s]+\.[A-Za-z0-9]{1,8})`/g;
|
|
442
|
+
const ABSOLUTE_PATH = /(?:^|[\s(])((?:[A-Za-z]:[\\/]|\/)[^\s"'<>|*?()[\]`]+\.[A-Za-z0-9]{1,8})/g;
|
|
443
|
+
|
|
444
|
+
function callArgs(call) {
|
|
445
|
+
let args = call?.function?.arguments ?? call?.arguments ?? call?.input;
|
|
446
|
+
if (typeof args === 'string') {
|
|
447
|
+
try {
|
|
448
|
+
args = JSON.parse(args);
|
|
449
|
+
} catch {
|
|
450
|
+
return {};
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return args && typeof args === 'object' ? args : {};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** The paths one tool call wrote or produced. */
|
|
457
|
+
function toolCallOutputs(call) {
|
|
458
|
+
const name = String(call?.function?.name || call?.name || '');
|
|
459
|
+
const args = callArgs(call);
|
|
460
|
+
const text = (value) => (typeof value === 'string' && value.trim() ? [value.trim()] : []);
|
|
461
|
+
if (name === 'edit') return text(args.file_path);
|
|
462
|
+
if (name === 'apply_patch') return [...String(args.patch || '').matchAll(PATCH_FILE)].map((match) => match[1].trim());
|
|
463
|
+
if (name === 'media') return ['generate', 'status'].includes(args.action) ? text(args.path) : [];
|
|
464
|
+
if (name === 'office') return OFFICE_WRITE_ACTIONS.has(args.action) ? [...text(args.output), ...text(args.path)] : [];
|
|
465
|
+
if (name === 'visualize') {
|
|
466
|
+
const input = args.input && typeof args.input === 'object' ? args.input : {};
|
|
467
|
+
const targets = args.action === 'open' && Array.isArray(input.targets) ? input.targets : [];
|
|
468
|
+
return [
|
|
469
|
+
...text(input.output),
|
|
470
|
+
...targets.filter((target) => typeof target === 'string' && !/^[a-z][a-z0-9+.-]*:\/\//i.test(target)),
|
|
471
|
+
];
|
|
472
|
+
}
|
|
473
|
+
return [];
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** File references in a reply's text: markdown link targets, backticked
|
|
477
|
+
* file names and absolute paths. URLs never count. */
|
|
478
|
+
function replyPaths(text) {
|
|
479
|
+
const found = [];
|
|
480
|
+
const add = (raw) => {
|
|
481
|
+
let value = String(raw || '').trim();
|
|
482
|
+
if (!value || /^(?:https?|mailto|data):/i.test(value)) return;
|
|
483
|
+
value = value
|
|
484
|
+
.replace(/^file:\/\/\/?/i, '')
|
|
485
|
+
.replace(/#.*$/, '')
|
|
486
|
+
.replace(/:\d+(?::\d+)?$/, '');
|
|
487
|
+
try {
|
|
488
|
+
value = decodeURI(value);
|
|
489
|
+
} catch {}
|
|
490
|
+
if (value) found.push(value);
|
|
491
|
+
};
|
|
492
|
+
for (const match of text.matchAll(MARKDOWN_LINK)) add(match[1]);
|
|
493
|
+
for (const match of text.matchAll(BACKTICK_PATH)) add(match[1]);
|
|
494
|
+
for (const match of text.matchAll(ABSOLUTE_PATH)) add(match[1]);
|
|
495
|
+
return found;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** The messages of one turn: from the user message that took submission
|
|
499
|
+
* `runId` (or, with `since`, the first user message at or after it) to the
|
|
500
|
+
* end; `messages` itself when neither finds one. */
|
|
501
|
+
export function turnSlice(messages, { runId = '', since = 0 } = {}) {
|
|
502
|
+
const list = Array.isArray(messages) ? messages : [];
|
|
503
|
+
let start = -1;
|
|
504
|
+
if (runId) {
|
|
505
|
+
start = list.findIndex((message) => {
|
|
506
|
+
if (message?.role !== 'user') return false;
|
|
507
|
+
const meta = message.meta || {};
|
|
508
|
+
const transcript = meta.transcript || {};
|
|
509
|
+
return [
|
|
510
|
+
transcript.checkpointId,
|
|
511
|
+
...(Array.isArray(transcript.submissionIds) ? transcript.submissionIds : []),
|
|
512
|
+
...(Array.isArray(meta.submissionIds) ? meta.submissionIds : []),
|
|
513
|
+
].includes(runId);
|
|
514
|
+
});
|
|
515
|
+
} else if (since > 0) {
|
|
516
|
+
start = list.findIndex((message) => message?.role === 'user' && Number(message?.meta?.transcript?.at) >= since);
|
|
517
|
+
}
|
|
518
|
+
return start >= 0 ? list.slice(start) : runId || since > 0 ? [] : list;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** The candidate artifact paths of a turn's messages, most relevant first:
|
|
522
|
+
* what its final reply names, then media/office/visualize outputs, then the
|
|
523
|
+
* files it edited. */
|
|
524
|
+
export function turnArtifactCandidates(messages) {
|
|
525
|
+
const list = Array.isArray(messages) ? messages : [];
|
|
526
|
+
const reply = list.findLast((message) => message?.role === 'assistant' && textOf(message.content).trim());
|
|
527
|
+
const produced = [];
|
|
528
|
+
const edited = [];
|
|
529
|
+
for (const message of list) {
|
|
530
|
+
if (message?.role !== 'assistant') continue;
|
|
531
|
+
const calls = Array.isArray(message.tool_calls)
|
|
532
|
+
? message.tool_calls
|
|
533
|
+
: Array.isArray(message.toolCalls)
|
|
534
|
+
? message.toolCalls
|
|
535
|
+
: [];
|
|
536
|
+
for (const call of calls) {
|
|
537
|
+
const name = String(call?.function?.name || call?.name || '');
|
|
538
|
+
(name === 'edit' || name === 'apply_patch' ? edited : produced).push(...toolCallOutputs(call));
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return [...(reply ? replyPaths(textOf(reply.content)) : []), ...produced, ...edited];
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* The files `messages` (one turn, turnSlice) produced, inside `roots`:
|
|
546
|
+
* `{ files, omitted }`, each file `{ path, name, kind, mimeType, size, id? }`
|
|
547
|
+
* — `id` (the attachment-store reference) when it fits a prompt's types and
|
|
548
|
+
* limits, else a path-only entry. Secret files, folders and anything outside
|
|
549
|
+
* the roots are left out silently; at most HANDOFF_LIMITS.count are kept.
|
|
550
|
+
*/
|
|
551
|
+
export function collectTurnArtifacts(messages, { cwd = '', roots = [] } = {}) {
|
|
552
|
+
const seen = new Set();
|
|
553
|
+
const files = [];
|
|
554
|
+
let omitted = 0;
|
|
555
|
+
const budget = createBudget();
|
|
556
|
+
for (const candidate of turnArtifactCandidates(messages)) {
|
|
557
|
+
// A candidate is a lexical path; its canonical form dedupes after the read.
|
|
558
|
+
const lexical = samePlatformPath(
|
|
559
|
+
isAbsolute(candidate) ? resolve(candidate) : resolve(cwd || process.cwd(), candidate)
|
|
560
|
+
);
|
|
561
|
+
if (seen.has(lexical)) continue;
|
|
562
|
+
seen.add(lexical);
|
|
563
|
+
let file;
|
|
564
|
+
try {
|
|
565
|
+
file = readContained(candidate, { cwd, roots, surface: 'artifact' });
|
|
566
|
+
} catch {
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
const key = samePlatformPath(file.path);
|
|
570
|
+
if (key !== lexical && seen.has(key)) continue;
|
|
571
|
+
seen.add(key);
|
|
572
|
+
if (files.length >= HANDOFF_LIMITS.count) {
|
|
573
|
+
omitted += 1;
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
const { type, bytes } = file;
|
|
577
|
+
const entry = {
|
|
578
|
+
path: file.path,
|
|
579
|
+
name: file.name,
|
|
580
|
+
kind: type?.kind || 'file',
|
|
581
|
+
mimeType: type?.mimeType || 'application/octet-stream',
|
|
582
|
+
size: file.size,
|
|
583
|
+
};
|
|
584
|
+
const storable = bytes && (type.kind !== 'image' || SUPPORTED_IMAGE_MIME_TYPES.has(type.mimeType));
|
|
585
|
+
if (storable && budget.take(type.kind, bytes.length)) {
|
|
586
|
+
try {
|
|
587
|
+
entry.id = storeAttachmentBuffer(bytes).attachmentRef;
|
|
588
|
+
} catch {}
|
|
589
|
+
}
|
|
590
|
+
files.push(entry);
|
|
591
|
+
}
|
|
592
|
+
return { files, omitted };
|
|
593
|
+
}
|