@vellumai/assistant 0.6.0 → 0.6.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/AGENTS.md +4 -0
- package/ARCHITECTURE.md +68 -15
- package/Dockerfile +2 -2
- package/bun.lock +6 -2
- package/docker-entrypoint.sh +42 -1
- package/docs/architecture/integrations.md +1 -1
- package/docs/architecture/memory.md +21 -24
- package/node_modules/@vellumai/ces-contracts/src/handles.ts +7 -9
- package/openapi.yaml +539 -4
- package/package.json +5 -1
- package/src/__tests__/anthropic-provider.test.ts +160 -95
- package/src/__tests__/app-dir-path-guard.test.ts +1 -0
- package/src/__tests__/app-executors.test.ts +47 -1
- package/src/__tests__/app-source-watcher.test.ts +159 -0
- package/src/__tests__/assistant-event-hub.test.ts +30 -0
- package/src/__tests__/checker.test.ts +138 -172
- package/src/__tests__/cli-command-risk-guard.test.ts +1 -1
- package/src/__tests__/config-schema.test.ts +5 -0
- package/src/__tests__/context-overflow-approval.test.ts +5 -5
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
- package/src/__tests__/conversation-agent-loop.test.ts +4 -51
- package/src/__tests__/conversation-analysis-routes.test.ts +169 -0
- package/src/__tests__/conversation-directories-parse.test.ts +105 -0
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
- package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
- package/src/__tests__/conversation-wipe.test.ts +2 -6
- package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
- package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
- package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
- package/src/__tests__/credential-execution-approval-bridge.test.ts +0 -2
- package/src/__tests__/date-context.test.ts +76 -210
- package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
- package/src/__tests__/file-list-tool.test.ts +219 -0
- package/src/__tests__/first-greeting.test.ts +1 -1
- package/src/__tests__/heartbeat-service.test.ts +180 -3
- package/src/__tests__/identity-routes.test.ts +328 -0
- package/src/__tests__/init-feature-flag-overrides.test.ts +167 -0
- package/src/__tests__/injection-block.test.ts +24 -0
- package/src/__tests__/inline-command-runner.test.ts +7 -5
- package/src/__tests__/install-skill-routing.test.ts +7 -6
- package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
- package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
- package/src/__tests__/llm-context-normalization.test.ts +18 -18
- package/src/__tests__/llm-context-route-provider.test.ts +101 -0
- package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
- package/src/__tests__/log-export-workspace.test.ts +257 -100
- package/src/__tests__/managed-credential-catalog-cli.test.ts +12 -14
- package/src/__tests__/mcp-abort-signal.test.ts +5 -0
- package/src/__tests__/mcp-client-auth.test.ts +5 -0
- package/src/__tests__/memory-recall-log-store.test.ts +132 -0
- package/src/__tests__/migration-export-streaming.test.ts +304 -0
- package/src/__tests__/migration-import-commit-http.test.ts +11 -10
- package/src/__tests__/mock-fetch.ts +87 -0
- package/src/__tests__/navigate-settings-tab.test.ts +14 -1
- package/src/__tests__/notification-broadcaster.test.ts +65 -0
- package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
- package/src/__tests__/onboarding-template-contract.test.ts +63 -14
- package/src/__tests__/parser.test.ts +32 -0
- package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
- package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
- package/src/__tests__/permission-mode-sse.test.ts +418 -0
- package/src/__tests__/permission-mode-store.test.ts +277 -0
- package/src/__tests__/permission-mode.test.ts +101 -0
- package/src/__tests__/pkb-autoinject.test.ts +96 -0
- package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
- package/src/__tests__/profiler-routes.test.ts +502 -0
- package/src/__tests__/profiler-run-store.test.ts +441 -0
- package/src/__tests__/proxy-approval-callback.test.ts +4 -75
- package/src/__tests__/registry.test.ts +1 -1
- package/src/__tests__/require-fresh-approval.test.ts +0 -2
- package/src/__tests__/sandbox-diagnostics.test.ts +1 -32
- package/src/__tests__/sandbox-host-parity.test.ts +5 -4
- package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
- package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
- package/src/__tests__/search-skills-unified.test.ts +4 -3
- package/src/__tests__/send-endpoint-busy.test.ts +42 -3
- package/src/__tests__/set-permission-mode.test.ts +274 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
- package/src/__tests__/skill-memory.test.ts +2 -783
- package/src/__tests__/strip-memory-injections.test.ts +187 -0
- package/src/__tests__/subagent-detail.test.ts +84 -0
- package/src/__tests__/subagent-disposal.test.ts +308 -0
- package/src/__tests__/subagent-manager-notify.test.ts +19 -10
- package/src/__tests__/subagent-notify-parent.test.ts +390 -0
- package/src/__tests__/subagent-role-registry.test.ts +108 -0
- package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
- package/src/__tests__/subagent-tools.test.ts +464 -4
- package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
- package/src/__tests__/task-memory-cleanup.test.ts +12 -12
- package/src/__tests__/terminal-sandbox.test.ts +1 -1
- package/src/__tests__/terminal-tools.test.ts +16 -29
- package/src/__tests__/test-preload.ts +18 -0
- package/src/__tests__/tool-domain-event-publisher.test.ts +0 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -8
- package/src/__tests__/tool-executor.test.ts +4 -27
- package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
- package/src/__tests__/top-level-renderer.test.ts +10 -13
- package/src/__tests__/transport-hints-queue.test.ts +77 -0
- package/src/__tests__/trust-store.test.ts +4 -4
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
- package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
- package/src/__tests__/workspace-migration-030-seed-pkb-autoinject.test.ts +168 -0
- package/src/__tests__/workspace-policy.test.ts +2 -7
- package/src/agent/loop.ts +6 -29
- package/src/approvals/guardian-request-resolvers.ts +24 -0
- package/src/avatar/traits-png-sync.ts +3 -3
- package/src/channels/types.ts +5 -0
- package/src/cli/__tests__/run-assistant-command.ts +56 -0
- package/src/cli/__tests__/unknown-command.test.ts +33 -0
- package/src/cli/commands/__tests__/email-download.test.ts +245 -0
- package/src/cli/commands/__tests__/email-list.test.ts +192 -0
- package/src/cli/commands/__tests__/email-register.test.ts +186 -0
- package/src/cli/commands/__tests__/email-send.test.ts +291 -0
- package/src/cli/commands/__tests__/email-status.test.ts +181 -0
- package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
- package/src/cli/commands/__tests__/routes.test.ts +562 -0
- package/src/cli/commands/conversations.ts +1 -8
- package/src/cli/commands/default-action.ts +68 -1
- package/src/cli/commands/email.ts +584 -835
- package/src/cli/commands/memory.ts +1 -34
- package/src/cli/commands/notifications.ts +7 -2
- package/src/cli/commands/oauth/__tests__/connect.test.ts +27 -0
- package/src/cli/commands/oauth/connect.ts +25 -5
- package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +1 -1
- package/src/cli/commands/routes.ts +396 -0
- package/src/cli/commands/skills.ts +130 -20
- package/src/cli/program.ts +11 -2
- package/src/cli.ts +1 -120
- package/src/config/assistant-feature-flags.ts +59 -55
- package/src/config/bundled-skills/app-builder/SKILL.md +91 -5
- package/src/config/bundled-skills/gmail/SKILL.md +13 -8
- package/src/config/bundled-skills/gmail/TOOLS.json +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -1
- package/src/config/bundled-skills/messaging/SKILL.md +7 -0
- package/src/config/bundled-skills/schedule/SKILL.md +22 -2
- package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
- package/src/config/bundled-skills/settings/TOOLS.json +1 -1
- package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
- package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
- package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +8 -3
- package/src/config/bundled-skills/slack/SKILL.md +2 -0
- package/src/config/bundled-skills/subagent/SKILL.md +43 -3
- package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
- package/src/config/env-registry.ts +63 -0
- package/src/config/feature-flag-registry.json +17 -1
- package/src/config/schema.ts +8 -0
- package/src/config/schemas/filing.ts +51 -0
- package/src/config/schemas/heartbeat.ts +15 -12
- package/src/config/schemas/memory-lifecycle.ts +12 -0
- package/src/config/schemas/security.ts +14 -0
- package/src/config/schemas/services.ts +8 -0
- package/src/credential-execution/approval-bridge.ts +0 -1
- package/src/credential-execution/managed-catalog.ts +3 -7
- package/src/daemon/app-source-watcher.ts +93 -0
- package/src/daemon/config-watcher.ts +85 -3
- package/src/daemon/context-overflow-approval.ts +0 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
- package/src/daemon/conversation-agent-loop.ts +179 -65
- package/src/daemon/conversation-attachments.ts +0 -1
- package/src/daemon/conversation-history.ts +4 -19
- package/src/daemon/conversation-lifecycle.ts +8 -14
- package/src/daemon/conversation-messaging.ts +3 -0
- package/src/daemon/conversation-process.ts +30 -8
- package/src/daemon/conversation-queue-manager.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +359 -308
- package/src/daemon/conversation-surfaces.ts +65 -0
- package/src/daemon/conversation-tool-setup.ts +44 -17
- package/src/daemon/conversation-workspace.ts +1 -2
- package/src/daemon/conversation.ts +19 -3
- package/src/daemon/date-context.ts +26 -53
- package/src/daemon/first-greeting.ts +1 -1
- package/src/daemon/handlers/conversations.ts +5 -7
- package/src/daemon/handlers/shared.test.ts +143 -0
- package/src/daemon/handlers/shared.ts +70 -5
- package/src/daemon/handlers/skills.ts +11 -18
- package/src/daemon/lifecycle.ts +220 -158
- package/src/daemon/message-types/conversations.ts +29 -6
- package/src/daemon/message-types/messages.ts +9 -2
- package/src/daemon/message-types/notifications.ts +12 -0
- package/src/daemon/message-types/schedules.ts +1 -0
- package/src/daemon/message-types/settings.ts +18 -0
- package/src/daemon/profiler-run-store.ts +557 -0
- package/src/daemon/server.ts +87 -10
- package/src/daemon/shutdown-handlers.ts +5 -0
- package/src/daemon/tool-side-effects.ts +23 -3
- package/src/daemon/transport-hints.ts +33 -0
- package/src/export/transcript-formatter.ts +148 -0
- package/src/filing/filing-service.ts +228 -0
- package/src/heartbeat/heartbeat-service.ts +96 -7
- package/src/index.ts +1 -1
- package/src/mcp/client.ts +6 -0
- package/src/mcp/mcp-oauth-provider.ts +149 -27
- package/src/memory/admin.ts +33 -32
- package/src/memory/app-store.ts +69 -0
- package/src/memory/conversation-bootstrap.ts +1 -1
- package/src/memory/conversation-crud.ts +151 -117
- package/src/memory/conversation-directories.ts +39 -0
- package/src/memory/conversation-group-migration.ts +66 -6
- package/src/memory/conversation-queries.ts +58 -12
- package/src/memory/conversation-title-service.ts +1 -0
- package/src/memory/db-init.ts +182 -376
- package/src/memory/embedding-local.ts +1 -1
- package/src/memory/graph/bootstrap.ts +75 -66
- package/src/memory/graph/capability-seed.ts +167 -17
- package/src/memory/graph/consolidation.ts +38 -4
- package/src/memory/graph/conversation-graph-memory.ts +133 -104
- package/src/memory/graph/extraction-job.ts +9 -4
- package/src/memory/graph/extraction.ts +66 -23
- package/src/memory/graph/graph-memory-state-store.ts +37 -0
- package/src/memory/graph/graph-search.ts +29 -15
- package/src/memory/graph/injection.ts +38 -8
- package/src/memory/graph/inspect.ts +12 -3
- package/src/memory/graph/retriever.ts +365 -262
- package/src/memory/graph/store.test.ts +48 -0
- package/src/memory/graph/store.ts +150 -11
- package/src/memory/graph/tool-handlers.ts +84 -209
- package/src/memory/graph/tools.ts +8 -52
- package/src/memory/graph/types.ts +24 -0
- package/src/memory/group-crud.ts +25 -9
- package/src/memory/job-handlers/cleanup.ts +44 -1
- package/src/memory/jobs-store.ts +70 -60
- package/src/memory/jobs-worker.ts +44 -28
- package/src/memory/llm-request-log-store.ts +96 -12
- package/src/memory/memory-recall-log-store.ts +49 -5
- package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
- package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
- package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
- package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
- package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
- package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
- package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
- package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
- package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
- package/src/memory/migrations/index.ts +8 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/conversations.ts +14 -0
- package/src/memory/schema/infrastructure.ts +8 -1
- package/src/memory/schema/memory-core.ts +0 -51
- package/src/memory/schema/memory-graph.ts +15 -0
- package/src/memory/task-memory-cleanup.ts +30 -11
- package/src/messaging/provider.ts +1 -1
- package/src/notifications/broadcaster.ts +6 -0
- package/src/notifications/conversation-pairing.ts +12 -4
- package/src/notifications/copy-composer.ts +86 -0
- package/src/notifications/decision-engine.ts +35 -0
- package/src/notifications/emit-signal.ts +14 -0
- package/src/notifications/signal.ts +11 -0
- package/src/oauth/platform-connection.test.ts +2 -2
- package/src/oauth/seed-providers.ts +1 -0
- package/src/permissions/checker.ts +15 -4
- package/src/permissions/defaults.ts +7 -8
- package/src/permissions/permission-mode-store.ts +180 -0
- package/src/permissions/permission-mode.ts +31 -0
- package/src/permissions/prompter.ts +0 -2
- package/src/permissions/workspace-policy.ts +9 -0
- package/src/platform/client.ts +1 -1
- package/src/prompts/system-prompt.ts +59 -7
- package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
- package/src/prompts/templates/BOOTSTRAP.md +76 -162
- package/src/prompts/templates/HEARTBEAT.md +3 -1
- package/src/prompts/templates/SOUL.md +30 -9
- package/src/prompts/templates/UPDATES.md +8 -0
- package/src/providers/anthropic/client.ts +107 -219
- package/src/runtime/assistant-event-hub.ts +22 -0
- package/src/runtime/auth/route-policy.ts +23 -0
- package/src/runtime/auth/token-service.ts +8 -0
- package/src/runtime/http-server.ts +32 -2
- package/src/runtime/http-types.ts +12 -1
- package/src/runtime/migrations/vbundle-builder.ts +389 -3
- package/src/runtime/migrations/vbundle-importer.ts +8 -6
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
- package/src/runtime/routes/app-management-routes.ts +1 -11
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
- package/src/runtime/routes/archive-utils.ts +29 -0
- package/src/runtime/routes/avatar-routes.ts +2 -9
- package/src/runtime/routes/btw-routes.ts +14 -1
- package/src/runtime/routes/conversation-analysis-routes.ts +185 -0
- package/src/runtime/routes/conversation-management-routes.ts +1 -14
- package/src/runtime/routes/conversation-query-routes.ts +49 -3
- package/src/runtime/routes/conversation-routes.ts +270 -44
- package/src/runtime/routes/group-routes.ts +22 -8
- package/src/runtime/routes/heartbeat-routes.ts +4 -10
- package/src/runtime/routes/identity-routes.ts +53 -18
- package/src/runtime/routes/llm-context-normalization.ts +14 -10
- package/src/runtime/routes/log-export/AGENTS.md +104 -0
- package/src/runtime/routes/log-export/__tests__/workspace-allowlist-error-contract.test.ts +103 -0
- package/src/runtime/routes/log-export/__tests__/workspace-allowlist.test.ts +716 -0
- package/src/runtime/routes/log-export/workspace-allowlist.ts +458 -0
- package/src/runtime/routes/log-export-routes.ts +41 -278
- package/src/runtime/routes/memory-item-routes.test.ts +168 -233
- package/src/runtime/routes/migration-routes.ts +18 -7
- package/src/runtime/routes/profiler-routes.ts +350 -0
- package/src/runtime/routes/schedule-routes.ts +27 -12
- package/src/runtime/routes/settings-routes.ts +95 -8
- package/src/runtime/routes/subagents-routes.ts +28 -7
- package/src/runtime/routes/user-route-dispatcher.ts +223 -0
- package/src/runtime/routes/user-routes.ts +41 -0
- package/src/runtime/routes/workspace-routes.ts +0 -1
- package/src/schedule/schedule-store.ts +30 -0
- package/src/schedule/scheduler.ts +45 -18
- package/src/skills/catalog-install.ts +10 -2
- package/src/skills/inline-command-runner.ts +12 -14
- package/src/skills/managed-store.ts +2 -2
- package/src/skills/skill-memory.ts +1 -293
- package/src/subagent/index.ts +13 -3
- package/src/subagent/manager.ts +308 -29
- package/src/subagent/types.ts +68 -0
- package/src/tasks/task-runner.ts +4 -4
- package/src/tools/apps/executors.ts +29 -4
- package/src/tools/filesystem/list.ts +93 -0
- package/src/tools/permission-checker.ts +78 -18
- package/src/tools/registry.ts +4 -0
- package/src/tools/schedule/create.ts +3 -0
- package/src/tools/schedule/list.ts +1 -0
- package/src/tools/schedule/update.ts +6 -0
- package/src/tools/secret-detection-handler.ts +0 -1
- package/src/tools/shared/filesystem/errors.ts +5 -0
- package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
- package/src/tools/shared/filesystem/types.ts +17 -0
- package/src/tools/shared/shell-output.ts +31 -2
- package/src/tools/skills/sandbox-runner.ts +3 -6
- package/src/tools/subagent/abort.ts +12 -2
- package/src/tools/subagent/message.ts +9 -2
- package/src/tools/subagent/notify-parent.ts +79 -0
- package/src/tools/subagent/read.ts +29 -8
- package/src/tools/subagent/resolve.ts +21 -0
- package/src/tools/subagent/spawn.ts +2 -0
- package/src/tools/subagent/status.ts +11 -1
- package/src/tools/system/avatar-generator.ts +3 -3
- package/src/tools/system/register.ts +23 -0
- package/src/tools/system/set-permission-mode.ts +103 -0
- package/src/tools/terminal/parser.ts +30 -5
- package/src/tools/terminal/safe-env.ts +16 -1
- package/src/tools/terminal/sandbox-diagnostics.ts +4 -4
- package/src/tools/terminal/sandbox.ts +4 -1
- package/src/tools/terminal/shell.ts +3 -5
- package/src/tools/tool-manifest.ts +6 -0
- package/src/tools/types.ts +2 -3
- package/src/util/logger.ts +1 -1
- package/src/util/platform.ts +50 -17
- package/src/watcher/provider-types.ts +1 -1
- package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
- package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
- package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
- package/src/workspace/migrations/029-seed-pkb.ts +85 -0
- package/src/workspace/migrations/030-seed-pkb-autoinject.ts +73 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/src/workspace/top-level-renderer.ts +5 -9
- package/src/__tests__/cli-memory.test.ts +0 -377
- package/src/__tests__/clipboard.test.ts +0 -88
- package/src/cli/cli-memory.ts +0 -179
- package/src/util/clipboard.ts +0 -34
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace allowlist module for the daemon log export endpoint.
|
|
3
|
+
*
|
|
4
|
+
* `POST /v1/export` collects audit DB rows, daemon logs, and a sanitized
|
|
5
|
+
* `config.json` snapshot. This module governs which subpaths of the user's
|
|
6
|
+
* workspace directory (`~/.vellum/workspace/`) are *opted in* to the export
|
|
7
|
+
* archive. The default is "nothing from the workspace ships" — every entry
|
|
8
|
+
* here must be justified against the rules in `./AGENTS.md`.
|
|
9
|
+
*
|
|
10
|
+
* The first allowlisted entry is `<workspace>/conversations/`, which honors
|
|
11
|
+
* both the time filter (via the parsed timestamp prefix on each conversation
|
|
12
|
+
* directory name) and the conversationId filter (via exact match on the id
|
|
13
|
+
* suffix). Directory names that don't match the canonical
|
|
14
|
+
* `<ISO-with-dashes>_<conversationId>` format are silently skipped (Rule 3).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
closeSync,
|
|
19
|
+
cpSync,
|
|
20
|
+
existsSync,
|
|
21
|
+
lstatSync,
|
|
22
|
+
mkdirSync,
|
|
23
|
+
openSync,
|
|
24
|
+
readdirSync,
|
|
25
|
+
readSync,
|
|
26
|
+
} from "node:fs";
|
|
27
|
+
import { join } from "node:path";
|
|
28
|
+
import { StringDecoder } from "node:string_decoder";
|
|
29
|
+
|
|
30
|
+
import { parseConversationDirName } from "../../../memory/conversation-directories.js";
|
|
31
|
+
import { getLogger } from "../../../util/logger.js";
|
|
32
|
+
import { getConversationsDir } from "../../../util/platform.js";
|
|
33
|
+
|
|
34
|
+
const log = getLogger("log-export-workspace");
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Maximum total bytes that the workspace allowlist may contribute to a
|
|
38
|
+
* single export archive. Mirrors `MAX_LOG_PAYLOAD_BYTES` in
|
|
39
|
+
* `log-export-routes.ts` so that the workspace section can never blow past
|
|
40
|
+
* the same 10 MB cap that already governs the daemon-logs section.
|
|
41
|
+
*/
|
|
42
|
+
export const MAX_WORKSPACE_PAYLOAD_BYTES = 10 * 1024 * 1024;
|
|
43
|
+
|
|
44
|
+
export interface CollectWorkspaceDataOptions {
|
|
45
|
+
/** Absolute path of the export staging directory. */
|
|
46
|
+
staging: string;
|
|
47
|
+
/** When set, restrict allowlisted entries to this conversation. */
|
|
48
|
+
conversationId?: string;
|
|
49
|
+
/** Lower bound (epoch ms, inclusive). */
|
|
50
|
+
startTime?: number;
|
|
51
|
+
/** Upper bound (epoch ms, inclusive). */
|
|
52
|
+
endTime?: number;
|
|
53
|
+
/** Override the default 10 MB cap (used in tests). */
|
|
54
|
+
maxBytes?: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface CollectWorkspaceDataResult {
|
|
58
|
+
/** Allowlisted entries that were copied to staging/workspace/. */
|
|
59
|
+
entries: Array<{
|
|
60
|
+
/** Allowlist entry name (e.g. "conversations"). */
|
|
61
|
+
entry: string;
|
|
62
|
+
/** Number of items (files or subdirs) copied. */
|
|
63
|
+
itemCount: number;
|
|
64
|
+
/** Total bytes copied for this entry. */
|
|
65
|
+
bytes: number;
|
|
66
|
+
/** Items skipped because the cap would be exceeded. */
|
|
67
|
+
skippedDueToCap: number;
|
|
68
|
+
}>;
|
|
69
|
+
totalBytes: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Walk a directory recursively and sum the sizes of every regular file
|
|
74
|
+
* underneath it. Bails out early once the running total would push the
|
|
75
|
+
* workspace cap over `remainingBudget` bytes — that way we never burn
|
|
76
|
+
* cycles totalling a multi-gigabyte directory only to discard it.
|
|
77
|
+
*
|
|
78
|
+
* Returns `null` to signal "this directory is too big to fit in the
|
|
79
|
+
* remaining budget"; returns the exact byte total otherwise.
|
|
80
|
+
*/
|
|
81
|
+
function dirSizeWithinBudget(
|
|
82
|
+
rootDir: string,
|
|
83
|
+
remainingBudget: number,
|
|
84
|
+
): number | null {
|
|
85
|
+
let total = 0;
|
|
86
|
+
const stack: string[] = [rootDir];
|
|
87
|
+
while (stack.length > 0) {
|
|
88
|
+
const current = stack.pop()!;
|
|
89
|
+
let entries: string[];
|
|
90
|
+
try {
|
|
91
|
+
entries = readdirSync(current);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
log.warn(
|
|
94
|
+
{ err, dir: current },
|
|
95
|
+
"Failed to read workspace directory while sizing; skipping",
|
|
96
|
+
);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
for (const name of entries) {
|
|
100
|
+
const child = join(current, name);
|
|
101
|
+
let stat: ReturnType<typeof lstatSync>;
|
|
102
|
+
try {
|
|
103
|
+
// Use lstat (not stat) so symlinks are NOT dereferenced. Without
|
|
104
|
+
// this, a symlink cycle inside a conversation directory (e.g.
|
|
105
|
+
// `loop -> .`) would cause the walker to recurse forever and
|
|
106
|
+
// hang `collectWorkspaceData`. With lstat, symlinks show up as
|
|
107
|
+
// symlinks — neither `isDirectory()` nor `isFile()` is true on
|
|
108
|
+
// the lstat result, so they're naturally skipped below.
|
|
109
|
+
stat = lstatSync(child);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
log.warn(
|
|
112
|
+
{ err, path: child },
|
|
113
|
+
"Failed to stat workspace path while sizing; skipping",
|
|
114
|
+
);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (stat.isDirectory()) {
|
|
118
|
+
stack.push(child);
|
|
119
|
+
} else if (stat.isFile()) {
|
|
120
|
+
total += stat.size;
|
|
121
|
+
if (total > remainingBudget) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return total;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Chunk size used by the streaming `messages.jsonl` reader. 64 KB is
|
|
132
|
+
* large enough to amortize syscall overhead but small enough to keep
|
|
133
|
+
* the synchronous read path off the event loop for any meaningful
|
|
134
|
+
* stretch.
|
|
135
|
+
*/
|
|
136
|
+
const MESSAGES_SCAN_CHUNK_BYTES = 64 * 1024;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Check whether a single JSONL line records a message whose `ts` falls
|
|
140
|
+
* in the `[startTime, endTime]` window. Returns `false` for malformed
|
|
141
|
+
* lines, missing/wrong-typed `ts` fields, and dates outside the window.
|
|
142
|
+
* Pulled out as a helper so the streaming reader can call it on each
|
|
143
|
+
* decoded line without duplicating the parsing logic.
|
|
144
|
+
*/
|
|
145
|
+
function lineMatchesWindow(
|
|
146
|
+
line: string,
|
|
147
|
+
startTime: number | undefined,
|
|
148
|
+
endTime: number | undefined,
|
|
149
|
+
): boolean {
|
|
150
|
+
if (!line) return false;
|
|
151
|
+
let record: { ts?: unknown };
|
|
152
|
+
try {
|
|
153
|
+
record = JSON.parse(line) as { ts?: unknown };
|
|
154
|
+
} catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
if (typeof record.ts !== "string") return false;
|
|
158
|
+
const ms = Date.parse(record.ts);
|
|
159
|
+
if (Number.isNaN(ms)) return false;
|
|
160
|
+
if (startTime !== undefined && ms < startTime) return false;
|
|
161
|
+
if (endTime !== undefined && ms > endTime) return false;
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Scan a conversation's `messages.jsonl` file and report whether any
|
|
167
|
+
* message's `ts` (an ISO 8601 string written by `conversation-disk-view`)
|
|
168
|
+
* falls inside the `[startTime, endTime]` window.
|
|
169
|
+
*
|
|
170
|
+
* Returns:
|
|
171
|
+
* - `true` if at least one message timestamp lies in the window.
|
|
172
|
+
* - `false` otherwise (including: file is missing, file is empty, every
|
|
173
|
+
* line fails to parse, or no parsed line lands in the window).
|
|
174
|
+
*
|
|
175
|
+
* Lines that fail to parse as JSON or whose `ts` is not a parseable date
|
|
176
|
+
* are silently skipped — they shouldn't be able to make the function
|
|
177
|
+
* throw, since the export pipeline must never crash on a malformed
|
|
178
|
+
* conversation file.
|
|
179
|
+
*
|
|
180
|
+
* The reader streams the file in fixed-size chunks (`MESSAGES_SCAN_CHUNK_BYTES`)
|
|
181
|
+
* via `readSync` and decodes UTF-8 across chunk boundaries with
|
|
182
|
+
* `StringDecoder`. It bails out as soon as it finds the first matching
|
|
183
|
+
* line, so the worst case for an in-window conversation is "one early
|
|
184
|
+
* hit", and the worst case for an out-of-window conversation is "read
|
|
185
|
+
* the whole file once" — without ever holding more than one chunk plus
|
|
186
|
+
* one in-progress line in memory.
|
|
187
|
+
*/
|
|
188
|
+
function conversationHasMessageInWindow(
|
|
189
|
+
conversationDir: string,
|
|
190
|
+
startTime: number | undefined,
|
|
191
|
+
endTime: number | undefined,
|
|
192
|
+
): boolean {
|
|
193
|
+
// No window means every message trivially "matches", but the only
|
|
194
|
+
// caller (`collectConversations`) already short-circuits in that case
|
|
195
|
+
// and never invokes this helper. Defensive check kept so the helper is
|
|
196
|
+
// safe to reuse.
|
|
197
|
+
if (startTime === undefined && endTime === undefined) return true;
|
|
198
|
+
|
|
199
|
+
const messagesPath = join(conversationDir, "messages.jsonl");
|
|
200
|
+
let fd: number;
|
|
201
|
+
try {
|
|
202
|
+
fd = openSync(messagesPath, "r");
|
|
203
|
+
} catch {
|
|
204
|
+
// Missing or unreadable messages file → no in-window evidence.
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const buffer = Buffer.alloc(MESSAGES_SCAN_CHUNK_BYTES);
|
|
209
|
+
const decoder = new StringDecoder("utf8");
|
|
210
|
+
let leftover = "";
|
|
211
|
+
try {
|
|
212
|
+
while (true) {
|
|
213
|
+
const bytesRead = readSync(fd, buffer, 0, buffer.length, null);
|
|
214
|
+
if (bytesRead === 0) break;
|
|
215
|
+
const text = leftover + decoder.write(buffer.subarray(0, bytesRead));
|
|
216
|
+
const lines = text.split("\n");
|
|
217
|
+
// The last segment may be a partial line — hold it back for the
|
|
218
|
+
// next chunk to complete.
|
|
219
|
+
leftover = lines.pop() ?? "";
|
|
220
|
+
for (const line of lines) {
|
|
221
|
+
if (lineMatchesWindow(line, startTime, endTime)) return true;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Drain any partial UTF-8 sequence the decoder is still holding,
|
|
225
|
+
// then check the final unterminated line (the file may not end with
|
|
226
|
+
// a newline).
|
|
227
|
+
const tail = leftover + decoder.end();
|
|
228
|
+
if (lineMatchesWindow(tail, startTime, endTime)) return true;
|
|
229
|
+
} finally {
|
|
230
|
+
try {
|
|
231
|
+
closeSync(fd);
|
|
232
|
+
} catch {
|
|
233
|
+
/* best-effort close */
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function collectConversations(
|
|
240
|
+
opts: CollectWorkspaceDataOptions,
|
|
241
|
+
result: CollectWorkspaceDataResult,
|
|
242
|
+
): void {
|
|
243
|
+
const maxBytes = opts.maxBytes ?? MAX_WORKSPACE_PAYLOAD_BYTES;
|
|
244
|
+
// Initialize the entry summary and push it onto `result.entries`
|
|
245
|
+
// immediately so the conversations entry is always present in the
|
|
246
|
+
// result, even if the candidate loop below throws partway through.
|
|
247
|
+
// The array holds a reference to this object, so all later mutations
|
|
248
|
+
// to `entry.itemCount`, `entry.bytes`, and `entry.skippedDueToCap`
|
|
249
|
+
// are visible to consumers via `result.entries`.
|
|
250
|
+
const entry = {
|
|
251
|
+
entry: "conversations",
|
|
252
|
+
itemCount: 0,
|
|
253
|
+
bytes: 0,
|
|
254
|
+
skippedDueToCap: 0,
|
|
255
|
+
};
|
|
256
|
+
result.entries.push(entry);
|
|
257
|
+
|
|
258
|
+
const sourceDir = getConversationsDir();
|
|
259
|
+
if (!existsSync(sourceDir)) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let names: string[];
|
|
264
|
+
try {
|
|
265
|
+
names = readdirSync(sourceDir);
|
|
266
|
+
} catch (err) {
|
|
267
|
+
log.warn(
|
|
268
|
+
{ err, sourceDir },
|
|
269
|
+
"Failed to read conversations directory; skipping conversations entry",
|
|
270
|
+
);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const destBase = join(opts.staging, "workspace", "conversations");
|
|
275
|
+
|
|
276
|
+
// First pass: parse the name, apply the conversationId filter, validate
|
|
277
|
+
// that the entry is a real directory (not a symlink, not a regular
|
|
278
|
+
// file), then apply the time-window filter (which may need to read
|
|
279
|
+
// `messages.jsonl`). Collect surviving candidates so we can sort them
|
|
280
|
+
// deterministically before applying the byte cap.
|
|
281
|
+
//
|
|
282
|
+
// The non-directory / symlink validation happens BEFORE the message
|
|
283
|
+
// scan so a canonical-named symlink can never coerce
|
|
284
|
+
// `conversationHasMessageInWindow` into reading from outside the
|
|
285
|
+
// `conversations/` boundary.
|
|
286
|
+
const candidates: Array<{
|
|
287
|
+
name: string;
|
|
288
|
+
parsed: { conversationId: string; createdAtMs: number };
|
|
289
|
+
}> = [];
|
|
290
|
+
for (const name of names) {
|
|
291
|
+
let parsed: ReturnType<typeof parseConversationDirName>;
|
|
292
|
+
try {
|
|
293
|
+
parsed = parseConversationDirName(name);
|
|
294
|
+
} catch (err) {
|
|
295
|
+
log.warn(
|
|
296
|
+
{ err, name },
|
|
297
|
+
"Failed to parse conversation directory name; skipping",
|
|
298
|
+
);
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (!parsed) continue; // Rule 3 — default deny non-canonical names.
|
|
302
|
+
|
|
303
|
+
if (
|
|
304
|
+
opts.conversationId !== undefined &&
|
|
305
|
+
parsed.conversationId !== opts.conversationId
|
|
306
|
+
) {
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const srcPath = join(sourceDir, name);
|
|
311
|
+
|
|
312
|
+
// Boundary guard: a canonical-looking entry must be a real directory
|
|
313
|
+
// under `conversations/`. Use `lstatSync` (not `statSync`) so
|
|
314
|
+
// symlinks are not dereferenced — a symlink with a canonical name
|
|
315
|
+
// pointing at an external directory must not be allowed to escape
|
|
316
|
+
// the allowlist boundary, neither for the time-window message scan
|
|
317
|
+
// below nor for the eventual `cpSync` copy. Symlinks and regular
|
|
318
|
+
// files are rejected explicitly here so the message scan and the
|
|
319
|
+
// copy loop only ever see real directories.
|
|
320
|
+
let srcStat: ReturnType<typeof lstatSync>;
|
|
321
|
+
try {
|
|
322
|
+
srcStat = lstatSync(srcPath);
|
|
323
|
+
} catch (err) {
|
|
324
|
+
log.warn({ err, srcPath }, "Failed to stat conversation entry; skipping");
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (srcStat.isSymbolicLink()) {
|
|
328
|
+
log.warn(
|
|
329
|
+
{ srcPath },
|
|
330
|
+
"Conversation entry is a symbolic link; skipping to preserve allowlist boundary",
|
|
331
|
+
);
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (!srcStat.isDirectory()) {
|
|
335
|
+
log.warn({ srcPath }, "Conversation entry is not a directory; skipping");
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Time-window filter: keep the conversation if EITHER its createdAt
|
|
340
|
+
// (parsed from the directory name) OR any individual message inside
|
|
341
|
+
// `messages.jsonl` falls in the requested window. This is the union
|
|
342
|
+
// semantics — a conversation that was started before the window but
|
|
343
|
+
// received messages during it should still ship, since the user
|
|
344
|
+
// running an export almost always wants to see the activity that
|
|
345
|
+
// happened during the window, not just conversations that were
|
|
346
|
+
// _created_ in it.
|
|
347
|
+
if (opts.startTime !== undefined || opts.endTime !== undefined) {
|
|
348
|
+
const createdAtInWindow =
|
|
349
|
+
(opts.startTime === undefined ||
|
|
350
|
+
parsed.createdAtMs >= opts.startTime) &&
|
|
351
|
+
(opts.endTime === undefined || parsed.createdAtMs <= opts.endTime);
|
|
352
|
+
if (!createdAtInWindow) {
|
|
353
|
+
// Fall back to scanning messages.jsonl for in-window activity.
|
|
354
|
+
// This is more expensive than the directory-name parse, so we
|
|
355
|
+
// only do it when the cheap check failed. The boundary guard
|
|
356
|
+
// above guarantees `srcPath` is a real in-allowlist directory,
|
|
357
|
+
// so the file path the scanner reads stays inside the allowlist.
|
|
358
|
+
let hasMessageInWindow: boolean;
|
|
359
|
+
try {
|
|
360
|
+
hasMessageInWindow = conversationHasMessageInWindow(
|
|
361
|
+
srcPath,
|
|
362
|
+
opts.startTime,
|
|
363
|
+
opts.endTime,
|
|
364
|
+
);
|
|
365
|
+
} catch (err) {
|
|
366
|
+
log.warn(
|
|
367
|
+
{ err, srcPath },
|
|
368
|
+
"Failed to scan messages.jsonl for window match; skipping",
|
|
369
|
+
);
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (!hasMessageInWindow) continue;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
candidates.push({ name, parsed });
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Newest first so cap-truncation keeps the most recent conversations.
|
|
380
|
+
candidates.sort((a, b) => b.parsed.createdAtMs - a.parsed.createdAtMs);
|
|
381
|
+
|
|
382
|
+
for (const { name } of candidates) {
|
|
383
|
+
const srcPath = join(sourceDir, name);
|
|
384
|
+
|
|
385
|
+
const remainingBudget = maxBytes - result.totalBytes;
|
|
386
|
+
let dirBytes: number | null;
|
|
387
|
+
try {
|
|
388
|
+
dirBytes = dirSizeWithinBudget(srcPath, remainingBudget);
|
|
389
|
+
} catch (err) {
|
|
390
|
+
log.warn(
|
|
391
|
+
{ err, srcPath },
|
|
392
|
+
"Failed to compute conversation directory size; skipping",
|
|
393
|
+
);
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (dirBytes === null) {
|
|
398
|
+
// Including this directory would exceed the workspace cap.
|
|
399
|
+
entry.skippedDueToCap += 1;
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
try {
|
|
404
|
+
mkdirSync(destBase, { recursive: true });
|
|
405
|
+
cpSync(srcPath, join(destBase, name), { recursive: true });
|
|
406
|
+
} catch (err) {
|
|
407
|
+
log.warn(
|
|
408
|
+
{ err, srcPath },
|
|
409
|
+
"Failed to copy conversation directory; skipping",
|
|
410
|
+
);
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
entry.itemCount += 1;
|
|
415
|
+
entry.bytes += dirBytes;
|
|
416
|
+
result.totalBytes += dirBytes;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Collect allowlisted workspace data into `<staging>/workspace/`.
|
|
422
|
+
*
|
|
423
|
+
* Currently the only allowlisted entry is `conversations/`. Future entries
|
|
424
|
+
* should follow the rules in `./AGENTS.md` (time filter, conversation
|
|
425
|
+
* filter, byte cap, registry update). The function never throws — all
|
|
426
|
+
* filesystem errors are logged at warn level so the rest of the export
|
|
427
|
+
* pipeline can continue regardless.
|
|
428
|
+
*/
|
|
429
|
+
export function collectWorkspaceData(
|
|
430
|
+
opts: CollectWorkspaceDataOptions,
|
|
431
|
+
): CollectWorkspaceDataResult {
|
|
432
|
+
const result: CollectWorkspaceDataResult = {
|
|
433
|
+
entries: [],
|
|
434
|
+
totalBytes: 0,
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
try {
|
|
438
|
+
collectConversations(opts, result);
|
|
439
|
+
} catch (err) {
|
|
440
|
+
log.warn(
|
|
441
|
+
{ err },
|
|
442
|
+
"Unexpected error while collecting workspace conversations entry",
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
log.info(
|
|
447
|
+
{
|
|
448
|
+
entries: result.entries,
|
|
449
|
+
totalBytes: result.totalBytes,
|
|
450
|
+
conversationId: opts.conversationId ?? null,
|
|
451
|
+
startTime: opts.startTime ?? null,
|
|
452
|
+
endTime: opts.endTime ?? null,
|
|
453
|
+
},
|
|
454
|
+
"Workspace allowlist collection complete",
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
return result;
|
|
458
|
+
}
|