@vellumai/assistant 0.11.1 → 0.11.2-staging.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/Dockerfile +1 -3
- package/README.md +1 -1
- package/eslint.config.mjs +28 -8
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
- package/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
- package/node_modules/@vellumai/slack-text/src/index.test.ts +48 -0
- package/node_modules/@vellumai/slack-text/src/index.ts +25 -6
- package/openapi.yaml +292 -5
- package/package.json +1 -1
- package/src/__tests__/agent-loop-resume-interrupted.test.ts +223 -0
- package/src/__tests__/byok-default-profile-ensure.test.ts +43 -0
- package/src/__tests__/cli-logger-boundary-guard.test.ts +87 -0
- package/src/__tests__/compaction-events.test.ts +139 -2
- package/src/__tests__/config-loader-backfill.test.ts +35 -3
- package/src/__tests__/config-schema.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +30 -0
- package/src/__tests__/conversation-agent-loop.test.ts +180 -7
- package/src/__tests__/conversation-error.test.ts +211 -4
- package/src/__tests__/conversation-options-turn-scoped-transport.test.ts +64 -0
- package/src/__tests__/conversation-queue.test.ts +126 -0
- package/src/__tests__/conversation-retry-route.test.ts +46 -7
- package/src/__tests__/conversation-slash.test.ts +4 -2
- package/src/__tests__/conversation-summarize-route.test.ts +7 -1
- package/src/__tests__/conversation-summarize-up-to.test.ts +71 -5
- package/src/__tests__/document-create-dedupe.test.ts +2 -1
- package/src/__tests__/document-find-replace.test.ts +2 -1
- package/src/__tests__/document-tool-security.test.ts +2 -1
- package/src/__tests__/document-update-default-surface.test.ts +2 -1
- package/src/__tests__/document-workspace-file.test.ts +467 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +290 -49
- package/src/__tests__/guardian-card-withdrawal.test.ts +91 -3
- package/src/__tests__/http-user-message-parity.test.ts +66 -0
- package/src/__tests__/list-messages-attachments.test.ts +87 -0
- package/src/__tests__/list-messages-provider-error.test.ts +143 -0
- package/src/__tests__/list-messages-system-card.test.ts +100 -0
- package/src/__tests__/llm-resolver.test.ts +19 -9
- package/src/__tests__/managed-profile-guard.test.ts +23 -0
- package/src/__tests__/notification-platform-adapter.test.ts +130 -2
- package/src/__tests__/notification-telegram-adapter.test.ts +6 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +45 -0
- package/src/__tests__/plugin-api-model-profiles.test.ts +10 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
- package/src/__tests__/provider-error-scenarios.test.ts +140 -0
- package/src/__tests__/provider-send-message-override-profile.test.ts +95 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +66 -3
- package/src/__tests__/scripted-turn-metadata-persistence.test.ts +209 -0
- package/src/__tests__/skills.test.ts +27 -0
- package/src/__tests__/slack-channels-routes.test.ts +0 -2
- package/src/__tests__/slack-share-routes.test.ts +0 -3
- package/src/__tests__/slack-users-routes.test.ts +0 -2
- package/src/__tests__/subagent-tools.test.ts +11 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +58 -0
- package/src/__tests__/tool-result-spool.test.ts +5 -4
- package/src/__tests__/turn-boundary-resolution.test.ts +53 -0
- package/src/__tests__/turn-events-store.test.ts +26 -0
- package/src/__tests__/ui-visual-surface.test.ts +695 -0
- package/src/__tests__/unified-turn-context-visible-app.test.ts +99 -0
- package/src/__tests__/visible-app-context.test.ts +189 -0
- package/src/__tests__/workspace-git-service.test.ts +173 -33
- package/src/__tests__/workspace-migration-137-repair-retired-fireworks-minimax-model-id.test.ts +157 -0
- package/src/__tests__/workspace-migration-138-backfill-home-feed-titles.test.ts +373 -0
- package/src/__tests__/workspace-migration-139-clear-renamed-cost-profile-label.test.ts +137 -0
- package/src/agent/loop.ts +36 -7
- package/src/api/events/context-window-usage.ts +31 -0
- package/src/api/events/notification-intent.ts +8 -0
- package/src/api/events/ui-surface-pending.ts +35 -0
- package/src/api/index.ts +14 -0
- package/src/api/responses/conversation-message.ts +25 -4
- package/src/api/surfaces.ts +90 -1
- package/src/approvals/guardian-card-withdrawal.ts +66 -31
- package/src/approvals/guardian-decision-primitive.ts +4 -0
- package/src/calls/__tests__/call-setup-router.test.ts +156 -26
- package/src/calls/__tests__/voice-session-bridge.test.ts +201 -12
- package/src/calls/call-setup-router.ts +80 -36
- package/src/calls/voice-session-bridge.ts +173 -26
- package/src/cli/commands/inference.help.ts +3 -3
- package/src/cli/commands/notifications.help.ts +3 -2
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +42 -128
- package/src/cli/commands/platform/__tests__/credits.test.ts +9 -78
- package/src/cli/commands/platform/__tests__/helpers.ts +90 -0
- package/src/cli/commands/platform/__tests__/invoices.test.ts +238 -0
- package/src/cli/commands/platform/__tests__/plans.test.ts +9 -88
- package/src/cli/commands/platform/__tests__/status.test.ts +12 -87
- package/src/cli/commands/platform/__tests__/subscription.test.ts +9 -86
- package/src/cli/commands/platform/index.help.ts +92 -0
- package/src/cli/commands/platform/index.ts +7 -0
- package/src/cli/commands/platform/invoices.ts +132 -0
- package/src/cli/commands/usage.help.ts +1 -1
- package/src/cli/lib/list-installed-plugins.ts +2 -1
- package/src/config/__tests__/default-profile-catalog.test.ts +7 -10
- package/src/config/__tests__/deployment-context-defaults.test.ts +27 -5
- package/src/config/assistant-feature-flags.ts +7 -2
- package/src/config/bundled-skills/app-builder/SKILL.md +3 -2
- package/src/config/bundled-skills/subagent/SKILL.md +3 -1
- package/src/config/bundled-skills/subagent/TOOLS.json +3 -3
- package/src/config/bundled-skills/visualize/SKILL.md +163 -0
- package/src/config/call-site-defaults.ts +3 -2
- package/src/config/default-profile-catalog.ts +75 -70
- package/src/config/default-profile-names.ts +13 -28
- package/src/config/env-registry.ts +1 -0
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/llm-resolver.ts +21 -5
- package/src/config/loader.ts +18 -11
- package/src/config/schemas/llm.ts +1 -9
- package/src/config/schemas/memory-retrospective.ts +9 -0
- package/src/config/schemas/monitoring.ts +28 -2
- package/src/config/schemas/workspace-git.ts +15 -0
- package/src/config/seed-inference-profiles.ts +19 -5
- package/src/context/post-turn-tool-result-truncation.ts +2 -2
- package/src/conversations/__tests__/message-consolidation.test.ts +54 -0
- package/src/conversations/message-consolidation.ts +17 -15
- package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +182 -0
- package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +181 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +90 -7
- package/src/daemon/conversation-agent-loop.ts +107 -22
- package/src/daemon/conversation-error.ts +169 -48
- package/src/daemon/conversation-messaging.ts +87 -4
- package/src/daemon/conversation-process.ts +57 -45
- package/src/daemon/conversation-runtime-assembly.ts +57 -0
- package/src/daemon/conversation-store.ts +36 -1
- package/src/daemon/conversation-surfaces.ts +68 -6
- package/src/daemon/conversation-turn-finalize.ts +76 -18
- package/src/daemon/conversation.ts +122 -27
- package/src/daemon/lifecycle.ts +9 -0
- package/src/daemon/message-types/conversations.ts +8 -0
- package/src/daemon/message-types/surfaces.ts +3 -0
- package/src/documents/document-store.ts +247 -10
- package/src/home/__tests__/feed-types.test.ts +43 -8
- package/src/home/__tests__/feed-writer.test.ts +59 -0
- package/src/home/feed-types.ts +34 -7
- package/src/home/feed-writer.ts +15 -6
- package/src/live-voice/__tests__/activity-label.test.ts +95 -0
- package/src/live-voice/__tests__/live-activity-reporter.test.ts +86 -5
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +310 -25
- package/src/live-voice/__tests__/live-voice-events.test.ts +4 -4
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +8 -4
- package/src/live-voice/__tests__/live-voice-vad.test.ts +9 -1
- package/src/live-voice/activity-label.ts +169 -0
- package/src/live-voice/live-activity-reporter.ts +35 -6
- package/src/live-voice/live-voice-session.ts +282 -41
- package/src/live-voice/protocol.ts +35 -0
- package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +84 -2
- package/src/messaging/providers/slack/adapter.ts +86 -19
- package/src/messaging/providers/slack/api.test.ts +85 -1
- package/src/messaging/providers/slack/api.ts +121 -288
- package/src/messaging/providers/slack/client.ts +20 -251
- package/src/messaging/providers/slack/send.test.ts +4 -9
- package/src/messaging/providers/slack/send.ts +1 -1
- package/src/messaging/providers/slack/types.ts +5 -0
- package/src/messaging/providers/slack/web-api-transport.test.ts +181 -0
- package/src/messaging/providers/slack/web-api-transport.ts +367 -0
- package/src/messaging/providers/slack/withdraw.ts +6 -14
- package/src/messaging/providers/telegram-bot/send.test.ts +31 -1
- package/src/messaging/providers/telegram-bot/send.ts +23 -2
- package/src/messaging/providers/telegram-bot/withdraw.test.ts +152 -0
- package/src/messaging/providers/telegram-bot/withdraw.ts +166 -0
- package/src/monitoring/__tests__/db-integrity-sample.test.ts +18 -4
- package/src/monitoring/__tests__/file-descriptors.test.ts +144 -0
- package/src/monitoring/file-descriptors.ts +262 -0
- package/src/monitoring/process-memory.ts +4 -11
- package/src/monitoring/resource-sampler.ts +5 -0
- package/src/monitoring/worker.ts +12 -0
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +740 -0
- package/src/notifications/__tests__/broadcaster.test.ts +347 -7
- package/src/notifications/__tests__/copy-composer.test.ts +53 -3
- package/src/notifications/__tests__/decision-engine.test.ts +349 -29
- package/src/notifications/__tests__/deterministic-checks.test.ts +21 -0
- package/src/notifications/__tests__/edit-notification.test.ts +330 -0
- package/src/notifications/__tests__/guardian-delivery-recorder.test.ts +71 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +170 -12
- package/src/notifications/adapters/macos.ts +3 -0
- package/src/notifications/adapters/platform.ts +90 -14
- package/src/notifications/adapters/telegram.ts +6 -4
- package/src/notifications/assistant-reply-producer.ts +219 -0
- package/src/notifications/broadcaster.ts +523 -317
- package/src/notifications/copy-composer.ts +27 -6
- package/src/notifications/decision-engine.ts +154 -77
- package/src/notifications/deterministic-checks.ts +6 -7
- package/src/notifications/edit-notification.ts +8 -4
- package/src/notifications/emit-signal.ts +49 -16
- package/src/notifications/guardian-delivery-recorder.ts +8 -2
- package/src/notifications/home-feed-side-effect.ts +43 -6
- package/src/notifications/notification-utils.ts +39 -4
- package/src/notifications/signal.ts +10 -0
- package/src/notifications/types.ts +17 -0
- package/src/persistence/bookmark-crud.ts +18 -9
- package/src/persistence/conversation-attention-store.ts +26 -0
- package/src/persistence/conversation-crud.ts +79 -32
- package/src/persistence/conversation-queries.ts +30 -15
- package/src/persistence/conversation-title-service.ts +5 -170
- package/src/persistence/conversation-types.ts +192 -1
- package/src/persistence/db-init.ts +14 -1
- package/src/persistence/db-maintenance.ts +5 -4
- package/src/persistence/embeddings/__tests__/plugin-index-qdrant-init.test.ts +219 -0
- package/src/persistence/embeddings/__tests__/plugin-index.test.ts +22 -0
- package/src/persistence/embeddings/__tests__/worker-script-version.test.ts +76 -0
- package/src/persistence/embeddings/embedding-local.ts +2 -1
- package/src/persistence/embeddings/embedding-runtime-manager.ts +29 -5
- package/src/persistence/embeddings/plugin-index.ts +84 -12
- package/src/persistence/migrations/360-add-document-workspace-path.test.ts +110 -0
- package/src/persistence/migrations/360-add-document-workspace-path.ts +39 -0
- package/src/persistence/planner-statistics.ts +14 -0
- package/src/persistence/schema/documents.ts +26 -11
- package/src/persistence/steps.ts +2 -0
- package/src/platform/client.test.ts +172 -2
- package/src/platform/client.ts +178 -55
- package/src/plugin-api/conversation-turn.ts +13 -0
- package/src/plugin-api/model-profiles.test.ts +6 -2
- package/src/plugins/defaults/memory/__tests__/bookmark-crud.test.ts +34 -0
- package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +22 -0
- package/src/plugins/defaults/memory/__tests__/jobs-store-enqueue-gate.test.ts +11 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +199 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +98 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +105 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +24 -2
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +524 -0
- package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +1 -1
- package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +9 -4
- package/src/plugins/defaults/memory/host-utils.ts +5 -0
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +105 -1
- package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +64 -5
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +36 -1
- package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +12 -3
- package/src/plugins/defaults/memory/src/__tests__/memory-v2-simulate-route.test.ts +1 -0
- package/src/plugins/defaults/memory/substrate/__tests__/skill-store.test.ts +195 -1
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +1 -1
- package/src/plugins/defaults/memory/substrate/skill-store.ts +167 -29
- package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +131 -0
- package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +1 -0
- package/src/plugins/defaults/memory/v2/activation-log-store.ts +4 -0
- package/src/plugins/defaults/memory/v2/injection.ts +48 -3
- package/src/plugins/defaults/memory/v2/rerank-local.ts +6 -2
- package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -1
- package/src/plugins/defaults/platform-hosted/routes/reengage.ts +1 -1
- package/src/plugins/defaults/turn-context/injectors.ts +1 -0
- package/src/plugins/defaults/turn-context/unified-turn-context.ts +26 -0
- package/src/plugins/types.ts +17 -0
- package/src/prompts/templates/system-sections.ts +2 -2
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +43 -2
- package/src/providers/__tests__/vellum-mismatch-routing.test.ts +8 -0
- package/src/providers/call-site-routing.ts +77 -14
- package/src/providers/connection-resolution.ts +42 -2
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +127 -1
- package/src/providers/inference/adapter-factory.ts +70 -12
- package/src/providers/model-catalog.ts +0 -11
- package/src/providers/model-intents.ts +11 -0
- package/src/providers/registry.ts +8 -0
- package/src/providers/retry.ts +85 -17
- package/src/providers/types.ts +8 -1
- package/src/runtime/__tests__/agent-wake.test.ts +181 -0
- package/src/runtime/agent-wake.ts +132 -9
- package/src/runtime/channel-approval-types.ts +25 -0
- package/src/runtime/routes/__tests__/connection-routes-vs-cli-parity.test.ts +2 -2
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +11 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +121 -9
- package/src/runtime/routes/__tests__/platform-invoice-routes.test.ts +392 -0
- package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +7 -0
- package/src/runtime/routes/__tests__/workspace-commit-routes.test.ts +63 -0
- package/src/runtime/routes/consolidation-routes.ts +1 -1
- package/src/runtime/routes/conversation-management-routes.ts +18 -6
- package/src/runtime/routes/conversation-query-routes.ts +14 -23
- package/src/runtime/routes/conversation-routes.ts +113 -8
- package/src/runtime/routes/credential-routes.ts +1 -5
- package/src/runtime/routes/documents-routes.ts +207 -12
- package/src/runtime/routes/identity-routes.ts +3 -93
- package/src/runtime/routes/inbound-stages/admission-policy.test.ts +31 -1
- package/src/runtime/routes/inbound-stages/admission-policy.ts +18 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +52 -3
- package/src/runtime/routes/notification-routes.ts +4 -1
- package/src/runtime/routes/platform-routes.ts +238 -3
- package/src/runtime/routes/playground/guard.ts +1 -2
- package/src/runtime/routes/slack-channel-routes.ts +2 -4
- package/src/runtime/routes/workspace-commit-routes.ts +49 -2
- package/src/runtime/routes/workspace-routes.ts +11 -12
- package/src/runtime/routes/workspace-utils.ts +49 -2
- package/src/runtime/services/conversation-serializer.ts +2 -4
- package/src/subagent/__tests__/consult-context-gating.test.ts +98 -0
- package/src/subagent/__tests__/consult-context-skills.test.ts +82 -0
- package/src/subagent/__tests__/consult-context.test.ts +84 -0
- package/src/subagent/__tests__/consult-prompt.test.ts +36 -0
- package/src/subagent/consult-context.ts +410 -0
- package/src/subagent/consult-prompt.ts +38 -5
- package/src/subagent/manager.ts +9 -4
- package/src/subagent/types.ts +8 -0
- package/src/telemetry/telemetry-event-sources.ts +7 -0
- package/src/telemetry/telemetry-wire-source.json +1 -1
- package/src/telemetry/telemetry-wire.generated.ts +1 -0
- package/src/telemetry/turn-events-store.ts +30 -1
- package/src/telemetry/types.ts +24 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
- package/src/tools/browser/pinned-tabs.ts +3 -1
- package/src/tools/subagent/spawn.ts +23 -1
- package/src/tools/terminal/safe-env.ts +1 -0
- package/src/tools/ui-surface/definitions.ts +39 -1
- package/src/tools/ui-surface/surface-shape-docs.ts +38 -4
- package/src/tools/ui-surface/visual-validation.ts +787 -0
- package/src/tools/workflows/run-workflow.test.ts +1 -0
- package/src/util/__tests__/short-title.test.ts +229 -0
- package/src/util/__tests__/worker-compute.test.ts +65 -0
- package/src/util/cgroup-cpu.ts +93 -0
- package/src/util/errors.ts +27 -0
- package/src/util/process-tree.ts +19 -0
- package/src/util/short-title.ts +189 -0
- package/src/util/worker-compute.ts +83 -0
- package/src/workspace/byok-default-profile-ensure.ts +25 -15
- package/src/workspace/git-service.ts +79 -30
- package/src/workspace/migrations/137-repair-retired-fireworks-minimax-model-id.ts +131 -0
- package/src/workspace/migrations/138-backfill-home-feed-titles.ts +179 -0
- package/src/workspace/migrations/139-clear-renamed-cost-profile-label.ts +92 -0
- package/src/workspace/migrations/registry.ts +6 -0
- package/src/plugins/defaults/memory/substrate/constants.ts +0 -8
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
normalizeTitle,
|
|
5
|
+
stripMarkdown,
|
|
6
|
+
stripThinkingTags,
|
|
7
|
+
truncateTitle,
|
|
8
|
+
} from "../short-title.js";
|
|
9
|
+
|
|
10
|
+
/** The character and word budgets `short-title.ts` documents and enforces. */
|
|
11
|
+
const MAX_TITLE_LENGTH = 40;
|
|
12
|
+
const MAX_TITLE_WORDS = 7;
|
|
13
|
+
|
|
14
|
+
describe("stripMarkdown", () => {
|
|
15
|
+
test("unwraps bold, italic, strikethrough, and code spans", () => {
|
|
16
|
+
expect(stripMarkdown("**Auth** Rewrite")).toBe("Auth Rewrite");
|
|
17
|
+
expect(stripMarkdown("__Auth__ Rewrite")).toBe("Auth Rewrite");
|
|
18
|
+
expect(stripMarkdown("*Auth* Rewrite")).toBe("Auth Rewrite");
|
|
19
|
+
expect(stripMarkdown("_Auth_ Rewrite")).toBe("Auth Rewrite");
|
|
20
|
+
expect(stripMarkdown("~~Auth~~ Rewrite")).toBe("Auth Rewrite");
|
|
21
|
+
expect(stripMarkdown("`Auth` Rewrite")).toBe("Auth Rewrite");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("keeps link text and drops the target", () => {
|
|
25
|
+
expect(stripMarkdown("[Auth Rewrite](https://example.com)")).toBe(
|
|
26
|
+
"Auth Rewrite",
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("removes heading markers", () => {
|
|
31
|
+
expect(stripMarkdown("## Auth Rewrite")).toBe("Auth Rewrite");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("preserves snake_case identifiers", () => {
|
|
35
|
+
expect(stripMarkdown("record_conversation_title")).toBe(
|
|
36
|
+
"record_conversation_title",
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("stripThinkingTags", () => {
|
|
42
|
+
test("removes paired thinking blocks and their contents", () => {
|
|
43
|
+
expect(stripThinkingTags("<thinking>hmm</thinking>Auth Rewrite")).toBe(
|
|
44
|
+
"Auth Rewrite",
|
|
45
|
+
);
|
|
46
|
+
expect(stripThinkingTags("<thought>hmm</thought>Auth Rewrite")).toBe(
|
|
47
|
+
"Auth Rewrite",
|
|
48
|
+
);
|
|
49
|
+
expect(stripThinkingTags("<think>hmm</think>Auth Rewrite")).toBe(
|
|
50
|
+
"Auth Rewrite",
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("removes unpaired tags but keeps the surrounding text", () => {
|
|
55
|
+
expect(stripThinkingTags("<thinking>Auth Rewrite")).toBe("Auth Rewrite");
|
|
56
|
+
expect(stripThinkingTags("Auth Rewrite</think>")).toBe("Auth Rewrite");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("removes colon-prefixed pseudo tags", () => {
|
|
60
|
+
expect(stripThinkingTags("<:anything>Auth Rewrite")).toBe("Auth Rewrite");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("leaves plain text untouched", () => {
|
|
64
|
+
expect(stripThinkingTags("Auth Rewrite")).toBe("Auth Rewrite");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("truncateTitle", () => {
|
|
69
|
+
test("leaves titles within the character limit untouched", () => {
|
|
70
|
+
const title = "Auth Middleware Rewrite";
|
|
71
|
+
expect(title.length).toBeLessThanOrEqual(MAX_TITLE_LENGTH);
|
|
72
|
+
expect(truncateTitle(title)).toBe(title);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("trims at a word boundary when few but long words exceed the char limit", () => {
|
|
76
|
+
const title = "Supercalifragilistic Authentication Middleware Rewrite";
|
|
77
|
+
expect(title.split(" ").length).toBeLessThanOrEqual(MAX_TITLE_WORDS);
|
|
78
|
+
expect(truncateTitle(title)).toBe("Supercalifragilistic Authentication");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("hard-slices a single word longer than the char limit", () => {
|
|
82
|
+
const title = "a".repeat(MAX_TITLE_LENGTH + 5);
|
|
83
|
+
expect(truncateTitle(title)).toBe("a".repeat(MAX_TITLE_LENGTH));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("keeps the first five words when the word limit is exceeded", () => {
|
|
87
|
+
const title =
|
|
88
|
+
"alpha bravo charlie delta echo foxtrot golf hotel india juliet";
|
|
89
|
+
expect(title.length).toBeGreaterThan(MAX_TITLE_LENGTH);
|
|
90
|
+
expect(title.split(" ").length).toBeGreaterThan(MAX_TITLE_WORDS);
|
|
91
|
+
expect(truncateTitle(title)).toBe("alpha bravo charlie delta echo");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("still honors the char limit when the first five words overflow it", () => {
|
|
95
|
+
const title =
|
|
96
|
+
"Authentication Infrastructure Modernization Deployment Verification Report Complete Today";
|
|
97
|
+
expect(title.length).toBeGreaterThan(MAX_TITLE_LENGTH);
|
|
98
|
+
expect(title.split(" ").length).toBeGreaterThan(MAX_TITLE_WORDS);
|
|
99
|
+
const result = truncateTitle(title);
|
|
100
|
+
expect(result).toBe("Authentication Infrastructure");
|
|
101
|
+
expect(result.length).toBeLessThanOrEqual(MAX_TITLE_LENGTH);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("normalizeTitle prose rejection", () => {
|
|
106
|
+
test("rejects multi-line output", () => {
|
|
107
|
+
expect(normalizeTitle("Auth Rewrite\nand more")).toBe("");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("rejects embedded transcript markers", () => {
|
|
111
|
+
expect(normalizeTitle("User: how do I log in")).toBe("");
|
|
112
|
+
expect(normalizeTitle("Assistant : here you go")).toBe("");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("rejects sentence-shaped clauses ending in terminal punctuation", () => {
|
|
116
|
+
expect(normalizeTitle("This is a topic about authentication.")).toBe("");
|
|
117
|
+
expect(normalizeTitle("What did they want to build here?")).toBe("");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("keeps short phrases that end in punctuation", () => {
|
|
121
|
+
expect(normalizeTitle("Auth Rewrite?")).toBe("Auth Rewrite?");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("rejects first-person reasoning openers", () => {
|
|
125
|
+
expect(normalizeTitle("I need to generate a title")).toBe("");
|
|
126
|
+
expect(normalizeTitle("I'll work through these files")).toBe("");
|
|
127
|
+
expect(normalizeTitle("I cannot title this")).toBe("");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("rejects imperative and deferral openers", () => {
|
|
131
|
+
expect(normalizeTitle("Let me summarize the request")).toBe("");
|
|
132
|
+
expect(normalizeTitle("Looking at the conversation")).toBe("");
|
|
133
|
+
expect(normalizeTitle("Based on the messages")).toBe("");
|
|
134
|
+
expect(normalizeTitle("Given the context")).toBe("");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("rejects infinitive openers", () => {
|
|
138
|
+
expect(normalizeTitle("To generate a good title")).toBe("");
|
|
139
|
+
expect(normalizeTitle("To summarize the discussion")).toBe("");
|
|
140
|
+
expect(normalizeTitle("To title this thread")).toBe("");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("rejects subject-led reasoning openers", () => {
|
|
144
|
+
expect(normalizeTitle("The user wants a summary")).toBe("");
|
|
145
|
+
expect(normalizeTitle("The conversation is about Docker")).toBe("");
|
|
146
|
+
expect(normalizeTitle("The assistant should reply")).toBe("");
|
|
147
|
+
expect(normalizeTitle("The title should be short")).toBe("");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("rejects preamble openers", () => {
|
|
151
|
+
expect(normalizeTitle("Here's a title")).toBe("");
|
|
152
|
+
expect(normalizeTitle("Here is a title")).toBe("");
|
|
153
|
+
expect(normalizeTitle("Here are the options")).toBe("");
|
|
154
|
+
expect(normalizeTitle("Sure, Auth Rewrite")).toBe("");
|
|
155
|
+
expect(normalizeTitle("Okay, Auth Rewrite")).toBe("");
|
|
156
|
+
expect(normalizeTitle("Ok, Auth Rewrite")).toBe("");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("accepts bare noun-phrase titles that share a subject word", () => {
|
|
160
|
+
expect(normalizeTitle("The Conversation API")).toBe("The Conversation API");
|
|
161
|
+
expect(normalizeTitle("The User Interface Redesign")).toBe(
|
|
162
|
+
"The User Interface Redesign",
|
|
163
|
+
);
|
|
164
|
+
expect(normalizeTitle("Auth Middleware Rewrite")).toBe(
|
|
165
|
+
"Auth Middleware Rewrite",
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("normalizeTitle", () => {
|
|
171
|
+
test("strips surrounding quotes", () => {
|
|
172
|
+
expect(normalizeTitle('"Auth Rewrite"')).toBe("Auth Rewrite");
|
|
173
|
+
expect(normalizeTitle("'Auth Rewrite'")).toBe("Auth Rewrite");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("strips markdown and thinking tags together", () => {
|
|
177
|
+
expect(normalizeTitle("<thinking>hmm</thinking> **Auth Rewrite**")).toBe(
|
|
178
|
+
"Auth Rewrite",
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("returns empty for blank input", () => {
|
|
183
|
+
expect(normalizeTitle("")).toBe("");
|
|
184
|
+
expect(normalizeTitle(" ")).toBe("");
|
|
185
|
+
expect(normalizeTitle("<thinking>hmm</thinking>")).toBe("");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("returns empty for leaked prose", () => {
|
|
189
|
+
expect(normalizeTitle("I need to generate a title for this")).toBe("");
|
|
190
|
+
expect(normalizeTitle("User: how do I log in")).toBe("");
|
|
191
|
+
expect(normalizeTitle("Auth Rewrite\nand more")).toBe("");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("returns empty for meta-failure titles regardless of case", () => {
|
|
195
|
+
const metaFailures = [
|
|
196
|
+
"missing context",
|
|
197
|
+
"no context",
|
|
198
|
+
"insufficient context",
|
|
199
|
+
"unclear context",
|
|
200
|
+
"empty context",
|
|
201
|
+
"no topic",
|
|
202
|
+
"unclear topic",
|
|
203
|
+
"unclear request",
|
|
204
|
+
"unclear message",
|
|
205
|
+
"empty conversation",
|
|
206
|
+
"empty message",
|
|
207
|
+
"no content",
|
|
208
|
+
];
|
|
209
|
+
for (const meta of metaFailures) {
|
|
210
|
+
expect(normalizeTitle(meta)).toBe("");
|
|
211
|
+
}
|
|
212
|
+
expect(normalizeTitle("Missing Context")).toBe("");
|
|
213
|
+
expect(normalizeTitle('"No Topic"')).toBe("");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("truncates an over-long accepted title", () => {
|
|
217
|
+
expect(
|
|
218
|
+
normalizeTitle("alpha bravo charlie delta echo foxtrot golf hotel india"),
|
|
219
|
+
).toBe("alpha bravo charlie delta echo");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("keeps a wordy title inside the character budget", () => {
|
|
223
|
+
const result = normalizeTitle(
|
|
224
|
+
"Authentication Infrastructure Modernization Deployment Verification Report Complete Today",
|
|
225
|
+
);
|
|
226
|
+
expect(result).toBe("Authentication Infrastructure");
|
|
227
|
+
expect(result.length).toBeLessThanOrEqual(MAX_TITLE_LENGTH);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
computeWorkerIntraOpThreads,
|
|
5
|
+
ONNX_INTRA_OP_THREADS_ENV,
|
|
6
|
+
WORKER_MAX_INTRA_OP_THREADS,
|
|
7
|
+
WORKER_MIN_INTRA_OP_THREADS,
|
|
8
|
+
workerComputeEnv,
|
|
9
|
+
} from "../worker-compute.js";
|
|
10
|
+
|
|
11
|
+
describe("computeWorkerIntraOpThreads", () => {
|
|
12
|
+
test("targets a quarter of the available cores", () => {
|
|
13
|
+
// 8 cores -> 2 threads, inside both clamps.
|
|
14
|
+
expect(computeWorkerIntraOpThreads(8)).toBe(2);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("clamps small machines up to the minimum", () => {
|
|
18
|
+
// 2 cores -> 0 raw, which ONNX would read as "unbounded"; clamped up.
|
|
19
|
+
expect(computeWorkerIntraOpThreads(2)).toBe(WORKER_MIN_INTRA_OP_THREADS);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("clamps large machines down to the maximum", () => {
|
|
23
|
+
// 64 cores -> 16 raw, clamped down.
|
|
24
|
+
expect(computeWorkerIntraOpThreads(64)).toBe(WORKER_MAX_INTRA_OP_THREADS);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("never returns a value ONNX would treat as unbounded", () => {
|
|
28
|
+
for (const cores of [0, -1, -100, NaN, Infinity]) {
|
|
29
|
+
expect(computeWorkerIntraOpThreads(cores)).toBeGreaterThanOrEqual(
|
|
30
|
+
WORKER_MIN_INTRA_OP_THREADS,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("fractional container quotas floor rather than round up", () => {
|
|
36
|
+
// A 1.5-core cgroup quota must not hand ONNX more than it has.
|
|
37
|
+
expect(computeWorkerIntraOpThreads(1.5)).toBe(WORKER_MIN_INTRA_OP_THREADS);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("workerComputeEnv", () => {
|
|
42
|
+
const saved = process.env[ONNX_INTRA_OP_THREADS_ENV];
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
if (saved === undefined) {
|
|
46
|
+
delete process.env[ONNX_INTRA_OP_THREADS_ENV];
|
|
47
|
+
} else {
|
|
48
|
+
process.env[ONNX_INTRA_OP_THREADS_ENV] = saved;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("computes a clamped thread cap", () => {
|
|
53
|
+
delete process.env[ONNX_INTRA_OP_THREADS_ENV];
|
|
54
|
+
const env = workerComputeEnv();
|
|
55
|
+
const threads = Number(env[ONNX_INTRA_OP_THREADS_ENV]);
|
|
56
|
+
expect(Number.isInteger(threads)).toBe(true);
|
|
57
|
+
expect(threads).toBeGreaterThanOrEqual(WORKER_MIN_INTRA_OP_THREADS);
|
|
58
|
+
expect(threads).toBeLessThanOrEqual(WORKER_MAX_INTRA_OP_THREADS);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("keeps an operator-provided override", () => {
|
|
62
|
+
process.env[ONNX_INTRA_OP_THREADS_ENV] = "7";
|
|
63
|
+
expect(workerComputeEnv()[ONNX_INTRA_OP_THREADS_ENV]).toBe("7");
|
|
64
|
+
});
|
|
65
|
+
});
|
package/src/util/cgroup-cpu.ts
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { readFileSync } from "node:fs";
|
|
10
|
+
import { availableParallelism, cpus } from "node:os";
|
|
10
11
|
|
|
12
|
+
import { getCpuLimit, getIsPlatform } from "../config/env-registry.js";
|
|
11
13
|
import { parseKeyedCounts } from "./cgroup-memory.js";
|
|
12
14
|
|
|
13
15
|
export interface ContainerCpuStat {
|
|
@@ -45,3 +47,94 @@ export function getContainerCpuStat(): ContainerCpuStat | null {
|
|
|
45
47
|
const raw = readCpuStatRaw();
|
|
46
48
|
return raw != null ? parseCpuStat(raw) : null;
|
|
47
49
|
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Parse a Kubernetes-style CPU string (e.g. "2000m", "1", "500m") into
|
|
53
|
+
* fractional cores. Returns null if the value is not a recognized format.
|
|
54
|
+
*/
|
|
55
|
+
export function parseK8sCpuCores(value: string): number | null {
|
|
56
|
+
const trimmed = value.trim();
|
|
57
|
+
const milliMatch = trimmed.match(/^(\d+)m$/);
|
|
58
|
+
if (milliMatch) {
|
|
59
|
+
const millis = parseInt(milliMatch[1], 10);
|
|
60
|
+
return millis > 0 ? millis / 1000 : null;
|
|
61
|
+
}
|
|
62
|
+
if (/^\d+(\.\d+)?$/.test(trimmed)) {
|
|
63
|
+
const num = parseFloat(trimmed);
|
|
64
|
+
return !isNaN(num) && num > 0 ? num : null;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Read the container's CPU core limit.
|
|
71
|
+
*
|
|
72
|
+
* Resolution order:
|
|
73
|
+
* 1. VELLUM_CPU_LIMIT env var (K8s resource format, e.g. "2000m" or "2").
|
|
74
|
+
* In platform mode the container runs under gVisor where cgroup files may
|
|
75
|
+
* report the node's CPU count rather than the sandbox limit.
|
|
76
|
+
* 2. cgroups v2 cpu.max (quota / period → fractional cores).
|
|
77
|
+
* 3. cgroups v1 cpu.cfs_quota_us / cpu.cfs_period_us.
|
|
78
|
+
* 4. os.cpus().length as last resort.
|
|
79
|
+
*/
|
|
80
|
+
export function getContainerCpuCores(): number {
|
|
81
|
+
// 1. Prefer the explicit env var set by the platform StatefulSet template.
|
|
82
|
+
try {
|
|
83
|
+
const envLimit = getCpuLimit();
|
|
84
|
+
if (envLimit) {
|
|
85
|
+
const parsed = parseK8sCpuCores(envLimit);
|
|
86
|
+
if (parsed !== null) {
|
|
87
|
+
return parsed;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
/* env var parsing failed – fall through */
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 2. Try cgroups v2: /sys/fs/cgroup/cpu.max contains "$MAX $PERIOD".
|
|
95
|
+
try {
|
|
96
|
+
const raw = readFileSync("/sys/fs/cgroup/cpu.max", "utf-8").trim();
|
|
97
|
+
if (!raw.startsWith("max")) {
|
|
98
|
+
const parts = raw.split(/\s+/);
|
|
99
|
+
const quota = parseInt(parts[0], 10);
|
|
100
|
+
const period = parseInt(parts[1], 10);
|
|
101
|
+
if (!isNaN(quota) && !isNaN(period) && period > 0 && quota > 0) {
|
|
102
|
+
const cores = quota / period;
|
|
103
|
+
// Sanity check: if the value looks like the node's full CPU count
|
|
104
|
+
// and we're on a platform pod, it's likely gVisor leaking the host value.
|
|
105
|
+
if (cores < cpus().length * 0.9 || !getIsPlatform()) {
|
|
106
|
+
return cores;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
/* not available */
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 3. Try cgroups v1.
|
|
115
|
+
try {
|
|
116
|
+
const quota = parseInt(
|
|
117
|
+
readFileSync("/sys/fs/cgroup/cpu/cpu.cfs_quota_us", "utf-8").trim(),
|
|
118
|
+
10,
|
|
119
|
+
);
|
|
120
|
+
const period = parseInt(
|
|
121
|
+
readFileSync("/sys/fs/cgroup/cpu/cpu.cfs_period_us", "utf-8").trim(),
|
|
122
|
+
10,
|
|
123
|
+
);
|
|
124
|
+
if (!isNaN(quota) && !isNaN(period) && period > 0 && quota > 0) {
|
|
125
|
+
const cores = quota / period;
|
|
126
|
+
if (cores < cpus().length * 0.9 || !getIsPlatform()) {
|
|
127
|
+
return cores;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
/* not available */
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 4. Fall back to the visible CPU count; 0 when even that syscall fails.
|
|
135
|
+
try {
|
|
136
|
+
return cpus().length || availableParallelism();
|
|
137
|
+
} catch {
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
}
|
package/src/util/errors.ts
CHANGED
|
@@ -123,6 +123,24 @@ export type ProviderErrorReason =
|
|
|
123
123
|
| "network_error"
|
|
124
124
|
| "unknown";
|
|
125
125
|
|
|
126
|
+
export type ProviderCredentialSource =
|
|
127
|
+
| "byok"
|
|
128
|
+
| "no-auth"
|
|
129
|
+
| "oauth-subscription"
|
|
130
|
+
| "vellum-managed";
|
|
131
|
+
|
|
132
|
+
export interface ProviderRouteAttribution {
|
|
133
|
+
connectionName?: string;
|
|
134
|
+
profileName?: string;
|
|
135
|
+
isManagedRoute?: boolean;
|
|
136
|
+
/**
|
|
137
|
+
* Credential surface used for this request. Finer than `isManagedRoute`:
|
|
138
|
+
* it separates personal provider keys, keyless endpoints, and subscription
|
|
139
|
+
* logins from each other so error copy can name what the user must fix.
|
|
140
|
+
*/
|
|
141
|
+
credentialSource?: ProviderCredentialSource;
|
|
142
|
+
}
|
|
143
|
+
|
|
126
144
|
export class ProviderError extends AssistantError {
|
|
127
145
|
/** Delay (in ms) suggested by the server's Retry-After header, if present. */
|
|
128
146
|
public readonly retryAfterMs?: number;
|
|
@@ -146,6 +164,8 @@ export class ProviderError extends AssistantError {
|
|
|
146
164
|
public readonly abortReason?: unknown;
|
|
147
165
|
/** Semantic failure classification stamped at the throw site. */
|
|
148
166
|
public readonly reason?: ProviderErrorReason;
|
|
167
|
+
/** Transport route selected for the failed provider call. */
|
|
168
|
+
public routeAttribution?: ProviderRouteAttribution;
|
|
149
169
|
|
|
150
170
|
constructor(
|
|
151
171
|
message: string,
|
|
@@ -174,6 +194,13 @@ export class ProviderError extends AssistantError {
|
|
|
174
194
|
this.abortReason = options?.abortReason;
|
|
175
195
|
this.reason = options?.reason;
|
|
176
196
|
}
|
|
197
|
+
|
|
198
|
+
attachRouteAttribution(attribution: ProviderRouteAttribution): void {
|
|
199
|
+
this.routeAttribution = {
|
|
200
|
+
...attribution,
|
|
201
|
+
...this.routeAttribution,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
177
204
|
}
|
|
178
205
|
|
|
179
206
|
export class ToolError extends AssistantError {
|
package/src/util/process-tree.ts
CHANGED
|
@@ -115,6 +115,25 @@ export function deriveName(command: string): string {
|
|
|
115
115
|
return argv0;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Redacted command descriptor for a live PID from `/proc/<pid>/cmdline`, or
|
|
120
|
+
* null when the process is gone or its `/proc` entry is unreadable. Kernel
|
|
121
|
+
* threads have an empty command line and read as `(unknown)`.
|
|
122
|
+
*
|
|
123
|
+
* The raw command line is read here but never returned: it can carry secrets
|
|
124
|
+
* (bearer tokens, API keys, database URLs) passed as process arguments, so
|
|
125
|
+
* callers that record process identity in snapshots or logs get only the
|
|
126
|
+
* {@link deriveName} descriptor.
|
|
127
|
+
*/
|
|
128
|
+
export function readProcessCommand(pid: number): string | null {
|
|
129
|
+
try {
|
|
130
|
+
const raw = readFileSync(`/proc/${pid}/cmdline`, "utf8");
|
|
131
|
+
return deriveName(raw.split("\0").filter(Boolean).join(" "));
|
|
132
|
+
} catch {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
118
137
|
/**
|
|
119
138
|
* Captures the plugin path segment(s) from a command whose executable/script
|
|
120
139
|
* path lives under a `plugins/<name>/` directory. Group 1 is the first segment
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Title hygiene helpers shared by every surface that renders a short,
|
|
3
|
+
* scannable conversation title: markdown/thinking-tag stripping, prose and
|
|
4
|
+
* meta-failure rejection, and length truncation.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const MAX_TITLE_LENGTH = 40;
|
|
8
|
+
const MAX_TITLE_WORDS = 7;
|
|
9
|
+
|
|
10
|
+
/** Word count a title exceeding `MAX_TITLE_WORDS` is cut back to. */
|
|
11
|
+
const TRIMMED_TITLE_WORDS = 5;
|
|
12
|
+
|
|
13
|
+
const META_FAILURE_TITLES = new Set([
|
|
14
|
+
"missing context",
|
|
15
|
+
"no context",
|
|
16
|
+
"insufficient context",
|
|
17
|
+
"unclear context",
|
|
18
|
+
"empty context",
|
|
19
|
+
"no topic",
|
|
20
|
+
"unclear topic",
|
|
21
|
+
"unclear request",
|
|
22
|
+
"unclear message",
|
|
23
|
+
"empty conversation",
|
|
24
|
+
"empty message",
|
|
25
|
+
"no content",
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
/** Reasoning/sentence openers that never start a legitimate topic title. */
|
|
29
|
+
const LEAKED_PROSE_PREFIXES = [
|
|
30
|
+
"i need to",
|
|
31
|
+
"i needed to",
|
|
32
|
+
"i should",
|
|
33
|
+
"i will",
|
|
34
|
+
"i'll",
|
|
35
|
+
"i can ",
|
|
36
|
+
"i can't",
|
|
37
|
+
"i cannot",
|
|
38
|
+
"i'm ",
|
|
39
|
+
"i am ",
|
|
40
|
+
"i've ",
|
|
41
|
+
"i have ",
|
|
42
|
+
"i'd ",
|
|
43
|
+
"i would",
|
|
44
|
+
"let me",
|
|
45
|
+
"looking at",
|
|
46
|
+
"based on",
|
|
47
|
+
"given the",
|
|
48
|
+
"to generate",
|
|
49
|
+
"to summarize",
|
|
50
|
+
"to title",
|
|
51
|
+
// Subject-led reasoning openers. A bare noun phrase ("The User Interface
|
|
52
|
+
// Redesign", "The Conversation API") is a valid title, so each subject only
|
|
53
|
+
// counts as leaked prose when a verb or possessive follows it, marking the
|
|
54
|
+
// output as a sentence rather than a topic.
|
|
55
|
+
"the user wants",
|
|
56
|
+
"the user asked",
|
|
57
|
+
"the user is",
|
|
58
|
+
"the user wanted",
|
|
59
|
+
"the user needs",
|
|
60
|
+
"the user said",
|
|
61
|
+
"the user has",
|
|
62
|
+
"the user would",
|
|
63
|
+
"the user's request",
|
|
64
|
+
"the conversation is",
|
|
65
|
+
"this conversation is",
|
|
66
|
+
"the conversation appears",
|
|
67
|
+
"the conversation seems",
|
|
68
|
+
"the conversation covers",
|
|
69
|
+
"the conversation discusses",
|
|
70
|
+
"the assistant should",
|
|
71
|
+
"the assistant is",
|
|
72
|
+
"the assistant wants",
|
|
73
|
+
"the assistant needs",
|
|
74
|
+
"the title should",
|
|
75
|
+
"the title is",
|
|
76
|
+
"the title would",
|
|
77
|
+
"the title for",
|
|
78
|
+
"here's ",
|
|
79
|
+
"here is ",
|
|
80
|
+
"here are ",
|
|
81
|
+
"sure,",
|
|
82
|
+
"okay,",
|
|
83
|
+
"ok,",
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
/** Strip common markdown formatting so titles render as plain text. */
|
|
87
|
+
export function stripMarkdown(text: string): string {
|
|
88
|
+
return text
|
|
89
|
+
.replace(/\*\*(.+?)\*\*/g, "$1") // **bold**
|
|
90
|
+
.replace(/__(.+?)__/g, "$1") // __bold__
|
|
91
|
+
.replace(/\*(.+?)\*/g, "$1") // *italic*
|
|
92
|
+
.replace(/(?<!\w)_(.+?)_(?!\w)/g, "$1") // _italic_ (word-boundary-aware to preserve snake_case)
|
|
93
|
+
.replace(/~~(.+?)~~/g, "$1") // ~~strikethrough~~
|
|
94
|
+
.replace(/`(.+?)`/g, "$1") // `code`
|
|
95
|
+
.replace(/\[(.+?)\]\(.+?\)/g, "$1") // [link](url)
|
|
96
|
+
.replace(/^#{1,6}\s+/gm, ""); // # headings
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Strip thinking tags so they don't bleed into generated titles. */
|
|
100
|
+
export function stripThinkingTags(text: string): string {
|
|
101
|
+
return text
|
|
102
|
+
.replace(/<thinking>[\s\S]*?<\/thinking>/gi, "")
|
|
103
|
+
.replace(/<thought>[\s\S]*?<\/thought>/gi, "")
|
|
104
|
+
.replace(/<think>[\s\S]*?<\/think>/gi, "")
|
|
105
|
+
.replace(/<thought>/gi, "")
|
|
106
|
+
.replace(/<\/thought>/gi, "")
|
|
107
|
+
.replace(/<thinking>/gi, "")
|
|
108
|
+
.replace(/<\/thinking>/gi, "")
|
|
109
|
+
.replace(/<think>/gi, "")
|
|
110
|
+
.replace(/<\/think>/gi, "")
|
|
111
|
+
.replace(/<:[^>]*>/gi, "");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Clamp a title to `MAX_TITLE_LENGTH` characters and `MAX_TITLE_WORDS` words.
|
|
116
|
+
* Both budgets apply: a wordy title is cut back to `TRIMMED_TITLE_WORDS` words
|
|
117
|
+
* and the survivors are still trimmed at a word boundary to fit the character
|
|
118
|
+
* budget, so the returned string is never longer than `MAX_TITLE_LENGTH`.
|
|
119
|
+
*/
|
|
120
|
+
export function truncateTitle(title: string): string {
|
|
121
|
+
if (title.length <= MAX_TITLE_LENGTH) {
|
|
122
|
+
return title;
|
|
123
|
+
}
|
|
124
|
+
const words = title.split(/\s+/);
|
|
125
|
+
const kept =
|
|
126
|
+
words.length > MAX_TITLE_WORDS
|
|
127
|
+
? words.slice(0, TRIMMED_TITLE_WORDS)
|
|
128
|
+
: words;
|
|
129
|
+
let result = "";
|
|
130
|
+
for (const word of kept) {
|
|
131
|
+
const candidate = result ? result + " " + word : word;
|
|
132
|
+
if (candidate.length > MAX_TITLE_LENGTH) {
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
result = candidate;
|
|
136
|
+
}
|
|
137
|
+
// Empty when even the first word overflows: hard-slice rather than return "".
|
|
138
|
+
return result || title.slice(0, MAX_TITLE_LENGTH);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Heuristic guard for title outputs that are clearly prose: the model
|
|
143
|
+
* reasoning aloud or replying to the conversation rather than naming it. A real
|
|
144
|
+
* title is a single-line short noun phrase, so we reject multi-line output,
|
|
145
|
+
* embedded transcript markers, leading reasoning openers, and sentence-shaped
|
|
146
|
+
* clauses. Deliberately tight: a false reject only costs a deterministic
|
|
147
|
+
* fallback title, while a false accept persists a broken one.
|
|
148
|
+
*/
|
|
149
|
+
function looksLikeLeakedProse(title: string): boolean {
|
|
150
|
+
if (/\n/.test(title)) {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
if (/\b(?:user|assistant)\s*:/i.test(title)) {
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
const lower = title.toLowerCase();
|
|
157
|
+
if (LEAKED_PROSE_PREFIXES.some((prefix) => lower.startsWith(prefix))) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
// Sentence-shaped: terminal punctuation on a multi-word clause.
|
|
161
|
+
if (/[.?!]$/.test(title) && title.split(/\s+/).length > 5) {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Clean a raw title into a short, scannable label. Returns "" when the input is
|
|
169
|
+
* empty, reads as leaked prose, or is a meta-failure title, signalling that
|
|
170
|
+
* callers should fall back to a deterministic title.
|
|
171
|
+
*/
|
|
172
|
+
export function normalizeTitle(raw: string): string {
|
|
173
|
+
let title = raw.trim().replace(/^["']|["']$/g, "");
|
|
174
|
+
title = stripMarkdown(title);
|
|
175
|
+
title = stripThinkingTags(title).trim();
|
|
176
|
+
if (!title) {
|
|
177
|
+
return "";
|
|
178
|
+
}
|
|
179
|
+
// Reject outputs that are the model reasoning aloud or continuing the
|
|
180
|
+
// conversation instead of naming it (e.g. "I need to generate a…", "I'll
|
|
181
|
+
// work through these files…"). Callers fall back to a deterministic title.
|
|
182
|
+
if (looksLikeLeakedProse(title)) {
|
|
183
|
+
return "";
|
|
184
|
+
}
|
|
185
|
+
if (META_FAILURE_TITLES.has(title.toLowerCase())) {
|
|
186
|
+
return "";
|
|
187
|
+
}
|
|
188
|
+
return truncateTitle(title);
|
|
189
|
+
}
|