@vellumai/assistant 0.12.5 → 0.12.6-dev.202609292116.41f7caa
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/.env.example +2 -0
- package/ARCHITECTURE.md +26 -3
- package/docs/automatic-done.md +32 -0
- package/docs/desktop-browser-cli.md +3 -2
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
- package/node_modules/@vellumai/environments/src/shell.test.ts +13 -1
- package/node_modules/@vellumai/environments/src/shell.ts +22 -1
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/status-contracts.test.ts +39 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +25 -0
- package/node_modules/@vellumai/service-contracts/package.json +2 -1
- package/node_modules/@vellumai/service-contracts/src/__tests__/mcp-oauth.test.ts +53 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/mcp-oauth.ts +60 -0
- package/openapi.yaml +270 -10
- package/package.json +1 -1
- package/src/__tests__/agent-loop.test.ts +76 -0
- package/src/__tests__/anthropic-provider.test.ts +2 -0
- package/src/__tests__/byok-default-profile-ensure.test.ts +7 -0
- package/src/__tests__/config-get-vision-flag.test.ts +1 -1
- package/src/__tests__/config-loader-validation-reset-notice.test.ts +2 -2
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +160 -154
- package/src/__tests__/conversation-evictor.test.ts +54 -0
- package/src/__tests__/conversation-pairing.test.ts +112 -0
- package/src/__tests__/document-append-idempotency.test.ts +13 -0
- package/src/__tests__/document-create-dedupe.test.ts +13 -0
- package/src/__tests__/document-find-replace.test.ts +13 -0
- package/src/__tests__/document-revision-history.test.ts +390 -0
- package/src/__tests__/document-revision.test.ts +425 -0
- package/src/__tests__/document-tool-security.test.ts +13 -0
- package/src/__tests__/document-update-default-surface.test.ts +13 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +270 -0
- package/src/__tests__/gateway-flag-listener.test.ts +59 -6
- package/src/__tests__/headless-browser-navigate.test.ts +20 -30
- package/src/__tests__/host-cu-proxy.test.ts +79 -0
- package/src/__tests__/host-cu-routes-targeted.test.ts +14 -5
- package/src/__tests__/host-shell-tool.test.ts +6 -2
- package/src/__tests__/identity-routes.test.ts +4 -3
- package/src/__tests__/list-messages-client-message-id.test.ts +43 -0
- package/src/__tests__/llm-catalog-parity.test.ts +4 -4
- package/src/__tests__/llm-schema.test.ts +1 -1
- package/src/__tests__/notification-platform-adapter.test.ts +83 -1
- package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
- package/src/__tests__/openai-provider.test.ts +38 -11
- package/src/__tests__/pricing.test.ts +11 -0
- package/src/__tests__/public-ingress-urls.test.ts +26 -0
- package/src/__tests__/retry-thinking-tool-choice.test.ts +47 -0
- package/src/__tests__/skill-execute-input.test.ts +11 -0
- package/src/__tests__/tool-approval-handler.test.ts +4 -0
- package/src/__tests__/tool-preview-lifecycle.test.ts +26 -0
- package/src/__tests__/usage-attribution.test.ts +31 -0
- package/src/__tests__/workspace-migration-133-collapse-provider-connections.test.ts +3 -3
- package/src/__tests__/workspace-migration-160-repair-retired-fireworks-model-ids.test.ts +262 -0
- package/src/__tests__/workspace-migration-161-repair-retired-fireworks-glm-5p2-model-id.test.ts +233 -0
- package/src/acp/agent-process.ts +6 -1
- package/src/acp/client-handler.ts +6 -1
- package/src/agent/loop-unparseable-tool-retry.test.ts +333 -0
- package/src/agent/loop.ts +161 -17
- package/src/api/events/document-editor-show.ts +5 -0
- package/src/api/events/document-editor-update.ts +10 -0
- package/src/api/events/tool-result.ts +1 -0
- package/src/calls/__tests__/call-controller.test.ts +273 -1
- package/src/calls/__tests__/media-stream-server-integration.test.ts +40 -0
- package/src/calls/__tests__/media-stream-stt-session.test.ts +33 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +54 -0
- package/src/calls/call-constants.ts +10 -0
- package/src/calls/call-controller.ts +359 -25
- package/src/calls/channel-admission-reader.ts +15 -9
- package/src/calls/media-stream-server.ts +18 -0
- package/src/calls/media-stream-stt-session.ts +30 -9
- package/src/calls/twilio-provider.ts +14 -31
- package/src/calls/voice-session-bridge.ts +18 -1
- package/src/cli/commands/browser.ts +1 -2
- package/src/cli/lib/__tests__/plugin-artifact.test.ts +23 -0
- package/src/cli/lib/__tests__/plugin-catalog-visibility.test.ts +2 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +16 -0
- package/src/cli/lib/bundled-marketplace.json +54 -2
- package/src/cli/lib/inspect-plugin.ts +15 -15
- package/src/cli/lib/list-installed-plugins.ts +29 -28
- package/src/cli/lib/plugin-artifact.ts +74 -63
- package/src/cli/lib/plugin-catalog-visibility.ts +1 -0
- package/src/cli/lib/plugin-details.ts +27 -42
- package/src/cli/lib/plugin-marketplace.ts +6 -8
- package/src/cli/lib/plugin-surfaces.ts +8 -7
- package/src/cli.ts +1 -3
- package/src/config/__tests__/chat-settings.test.ts +121 -0
- package/src/config/assistant-feature-flags.ts +28 -23
- package/src/config/bundled-skills/computer-use/SKILL.md +20 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +11 -11
- package/src/config/chat-settings.ts +100 -0
- package/src/config/default-profile-catalog.ts +1 -1
- package/src/config/feature-flag-cache.ts +7 -25
- package/src/config/feature-flag-registry.json +9 -9
- package/src/config/loader.ts +5 -1
- package/src/config/profile-text-generation.ts +25 -21
- package/src/config/schemas/conversations.ts +14 -0
- package/src/config/schemas/llm.ts +1 -0
- package/src/config/schemas/notifications.ts +8 -1
- package/src/contacts/gateway-channel-read.ts +1 -1
- package/src/conversations/__tests__/auto-archive-activity.test.ts +136 -0
- package/src/conversations/__tests__/auto-archive.test.ts +304 -0
- package/src/conversations/auto-archive-activity.ts +61 -0
- package/src/conversations/auto-archive.ts +201 -0
- package/src/daemon/computer-use-observation.test.ts +81 -0
- package/src/daemon/computer-use-observation.ts +23 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +18 -2
- package/src/daemon/conversation-agent-loop.ts +14 -6
- package/src/daemon/conversation-evictor.ts +50 -16
- package/src/daemon/conversation-messaging.ts +4 -0
- package/src/daemon/conversation-process.ts +15 -0
- package/src/daemon/conversation-queue-manager.ts +2 -0
- package/src/daemon/conversation.ts +2 -0
- package/src/daemon/doordash-steps.ts +1 -3
- package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +43 -1
- package/src/daemon/handlers/__tests__/config-channels.test.ts +1 -1
- package/src/daemon/handlers/config-a2a.ts +75 -2
- package/src/daemon/handlers/shared.ts +13 -2
- package/src/daemon/host-cu-proxy.ts +34 -3
- package/src/daemon/lifecycle.ts +15 -2
- package/src/daemon/process-message.ts +5 -0
- package/src/daemon/shutdown-handlers.ts +3 -0
- package/src/daemon/turn-finalization.ts +4 -0
- package/src/desktop/desktop-browser-endpoint.ts +11 -2
- package/src/desktop/desktop-chrome-policy.test.ts +1 -0
- package/src/desktop/desktop-chrome-policy.ts +7 -1
- package/src/desktop/desktop-computer-use.test.ts +14 -4
- package/src/desktop/desktop-session-manager.ts +5 -3
- package/src/documents/document-revisions-store.ts +153 -0
- package/src/documents/document-store.ts +471 -133
- package/src/followups/followup-store.ts +0 -4
- package/src/i18n/__tests__/i18n.test.ts +25 -0
- package/src/i18n/assistant-text.ts +18 -0
- package/src/i18n/index.ts +1 -0
- package/src/i18n/messages.ts +11 -0
- package/src/inbound/__tests__/oauth-callback-url.test.ts +25 -2
- package/src/inbound/oauth-callback-url.ts +30 -1
- package/src/inbound/public-ingress-urls.ts +12 -2
- package/src/ipc/gateway-client.test.ts +40 -0
- package/src/ipc/gateway-client.ts +12 -29
- package/src/ipc/gateway-flag-listener.ts +7 -10
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +71 -0
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +83 -0
- package/src/live-voice/__tests__/protocol.test.ts +77 -0
- package/src/live-voice/__tests__/share-targets.test.ts +163 -0
- package/src/live-voice/live-voice-session.ts +23 -6
- package/src/live-voice/protocol.ts +22 -0
- package/src/live-voice/share-targets.ts +225 -0
- package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +144 -3
- package/src/mcp/__tests__/mcp-oauth-provider.test.ts +2 -0
- package/src/mcp/client.ts +1 -19
- package/src/mcp/mcp-oauth-provider.ts +66 -49
- package/src/media/openrouter-image-service.ts +39 -22
- package/src/monitoring/__tests__/oom-inheritance-sweep.test.ts +86 -0
- package/src/monitoring/__tests__/oom-kill-reporter.test.ts +374 -0
- package/src/monitoring/control.ts +2 -1
- package/src/monitoring/oom-inheritance-sweep.ts +96 -0
- package/src/monitoring/oom-kill-reporter.ts +469 -0
- package/src/monitoring/resource-sampler.ts +30 -0
- package/src/notifications/README.md +53 -4
- package/src/notifications/__tests__/assistant-initiated-thread.test.ts +254 -0
- package/src/notifications/__tests__/broadcaster.test.ts +558 -6
- package/src/notifications/__tests__/completion-policy.test.ts +37 -0
- package/src/notifications/__tests__/from-me-alert-integration.test.ts +192 -0
- package/src/notifications/adapters/platform.ts +14 -0
- package/src/notifications/assistant-initiated-thread.ts +101 -0
- package/src/notifications/broadcaster.ts +94 -16
- package/src/notifications/chat-reply-policy.ts +11 -0
- package/src/notifications/completion-policy.ts +12 -2
- package/src/notifications/conversation-pairing.ts +15 -70
- package/src/notifications/emit-signal.ts +34 -3
- package/src/notifications/types.ts +2 -0
- package/src/oauth/connect-orchestrator.ts +1 -2
- package/src/persistence/__tests__/conversation-auto-archive.test.ts +330 -0
- package/src/persistence/conversation-auto-archive.ts +170 -0
- package/src/persistence/conversation-crud.ts +2 -1
- package/src/persistence/conversation-queries.ts +15 -8
- package/src/persistence/embeddings/embed.test.ts +96 -0
- package/src/persistence/embeddings/embed.ts +20 -2
- package/src/persistence/embeddings/embedding-gemini.test.ts +39 -3
- package/src/persistence/embeddings/embedding-gemini.ts +39 -1
- package/src/persistence/embeddings/embedding-local.ts +6 -0
- package/src/persistence/embeddings/qdrant-manager.ts +5 -0
- package/src/persistence/job-utils.ts +10 -0
- package/src/persistence/migrations/384-add-document-revision.test.ts +83 -0
- package/src/persistence/migrations/384-add-document-revision.ts +21 -0
- package/src/persistence/migrations/385-create-document-revisions.test.ts +67 -0
- package/src/persistence/migrations/385-create-document-revisions.ts +28 -0
- package/src/persistence/migrations/386-conversations-last-reopened-at.ts +10 -0
- package/src/persistence/migrations/__tests__/386-conversations-last-reopened-at.test.ts +32 -0
- package/src/persistence/schema/conversations.ts +1 -0
- package/src/persistence/schema/documents.ts +20 -0
- package/src/persistence/steps.ts +6 -0
- package/src/platform/managed-speech.ts +42 -46
- package/src/plugin-api/vision-support.test.ts +21 -21
- package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +11 -6
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +1 -1
- package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
- package/src/providers/__tests__/unparseable-tool-args.test.ts +25 -0
- package/src/providers/anthropic/client.ts +1 -1
- package/src/providers/fireworks/client.test.ts +168 -0
- package/src/providers/fireworks/client.ts +24 -0
- package/src/providers/inference/adapter-factory.ts +6 -0
- package/src/providers/ionet/client.test.ts +143 -0
- package/src/providers/ionet/client.ts +37 -0
- package/src/providers/model-catalog.ts +141 -84
- package/src/providers/model-intents.ts +4 -4
- package/src/providers/openai/chat-completions-provider.ts +33 -2
- package/src/providers/retry.ts +56 -4
- package/src/providers/search-provider-catalog.ts +10 -0
- package/src/providers/types.ts +10 -2
- package/src/providers/unparseable-tool-args.ts +29 -5
- package/src/providers/usage-tracking.ts +5 -3
- package/src/runtime/assistant-event-hub.ts +23 -0
- package/src/runtime/auth/__tests__/guard-tests.test.ts +0 -7
- package/src/runtime/auth/__tests__/route-policy.test.ts +1 -1
- package/src/runtime/channel-reply-delivery.ts +2 -0
- package/src/runtime/channel-retry-sweep.ts +1 -2
- package/src/runtime/http-types.ts +3 -0
- package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +35 -10
- package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +48 -0
- package/src/runtime/migrations/vbundle-builder.ts +19 -3
- package/src/runtime/migrations/vbundle-streaming-importer.ts +14 -19
- package/src/runtime/migrations/vbundle-streaming-validator.ts +14 -7
- package/src/runtime/migrations/vbundle-validator.ts +54 -1
- package/src/runtime/pending-interactions.ts +20 -2
- package/src/runtime/routes/__tests__/contact-routes.test.ts +5 -3
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +36 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +375 -2
- package/src/runtime/routes/__tests__/inference-send-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +12 -0
- package/src/runtime/routes/contact-routes.ts +2 -2
- package/src/runtime/routes/conversation-list-routes.ts +10 -2
- package/src/runtime/routes/conversation-query-routes.ts +48 -10
- package/src/runtime/routes/conversation-routes.ts +7 -0
- package/src/runtime/routes/documents-routes.ts +185 -47
- package/src/runtime/routes/events-routes.ts +1 -3
- package/src/runtime/routes/host-app-control-routes.ts +1 -0
- package/src/runtime/routes/host-bash-routes.ts +1 -0
- package/src/runtime/routes/host-browser-routes.ts +15 -0
- package/src/runtime/routes/host-cu-routes.ts +4 -1
- package/src/runtime/routes/host-file-routes.ts +1 -0
- package/src/runtime/routes/host-proxy-result-binding.test.ts +83 -0
- package/src/runtime/routes/host-proxy-result-binding.ts +25 -0
- package/src/runtime/routes/host-transfer-routes.ts +1 -0
- package/src/runtime/routes/identity-routes.ts +3 -3
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -0
- package/src/runtime/routes/integrations/a2a.ts +4 -1
- package/src/runtime/routes/migration-routes.ts +3 -1
- package/src/runtime/routes/notification-routes.ts +1 -2
- package/src/runtime/routes/ui-snapshot-routes.test.ts +1 -0
- package/src/runtime/routes/user-route-dispatcher.ts +5 -1
- package/src/runtime/verification-templates.ts +1 -2
- package/src/sequence/analytics.ts +0 -4
- package/src/sequence/engine.ts +1 -11
- package/src/sequence/guardrails.ts +1 -6
- package/src/sequence/importer.ts +0 -8
- package/src/sequence/store.ts +0 -10
- package/src/signals/user-message.ts +0 -6
- package/src/skills/catalog-install.ts +52 -23
- package/src/tools/browser/browser-execution.ts +20 -23
- package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +1 -3
- package/src/tools/document/document-tool.ts +13 -11
- package/src/tools/executor.ts +1 -2
- package/src/tools/host-terminal/host-shell.ts +4 -2
- package/src/tools/network/__tests__/web-search.test.ts +105 -0
- package/src/tools/network/web-fetch.ts +2 -4
- package/src/tools/network/web-search.ts +239 -12
- package/src/tools/shared/filesystem/format-diff.ts +1 -2
- package/src/tools/shared/shell-output.test.ts +42 -0
- package/src/tools/shared/shell-output.ts +27 -4
- package/src/tools/skills/execute.ts +10 -7
- package/src/tools/terminal/shell.ts +3 -3
- package/src/tools/tool-approval-handler.ts +9 -1
- package/src/tools/ui-surface/visual-validation.ts +1 -2
- package/src/usage/attribution.ts +18 -1
- package/src/util/__tests__/oom-priority.test.ts +55 -0
- package/src/util/__tests__/worker-memory.test.ts +16 -0
- package/src/util/debounce.ts +0 -4
- package/src/util/oom-priority.ts +79 -0
- package/src/util/platform.ts +0 -2
- package/src/util/plugin-manifest.ts +5 -7
- package/src/util/pricing.ts +25 -0
- package/src/util/worker-memory.ts +9 -3
- package/src/util/worker-process.ts +8 -0
- package/src/workspace/byok-default-profile-ensure.ts +11 -0
- package/src/workspace/hatched-date.ts +11 -4
- package/src/workspace/migrations/160-repair-retired-fireworks-model-ids.ts +209 -0
- package/src/workspace/migrations/161-repair-retired-fireworks-glm-5p2-model-id.ts +197 -0
- package/src/workspace/migrations/registry.ts +4 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
createOomKillReporter,
|
|
8
|
+
type KernelLogEntry,
|
|
9
|
+
oomKillsFromEntries,
|
|
10
|
+
parseDmesgJson,
|
|
11
|
+
parseDmesgText,
|
|
12
|
+
parseOwnContainerIds,
|
|
13
|
+
} from "../oom-kill-reporter.js";
|
|
14
|
+
import type { ResourceSample } from "../resource-sample-types.js";
|
|
15
|
+
|
|
16
|
+
const OUR_ID =
|
|
17
|
+
"de939a251b36f11a400aab40a43b756ca376056239227aec2795953ca219e7d5";
|
|
18
|
+
const OTHER_ID =
|
|
19
|
+
"0f4c3b2a1908f7e6d5c4b3a29180f7e6d5c4b3a29180f7e6d5c4b3a29180f7e6";
|
|
20
|
+
const memcg = (id: string) =>
|
|
21
|
+
`/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-poda5d2033b.slice/cri-containerd-${id}.scope`;
|
|
22
|
+
|
|
23
|
+
const header = (pid: number, comm: string, id: string) =>
|
|
24
|
+
`oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=cri-containerd-${id}.scope,mems_allowed=0,oom_memcg=${memcg(id)},task_memcg=${memcg(id)},task=${comm},pid=${pid},uid=0`;
|
|
25
|
+
const killed = (
|
|
26
|
+
pid: number,
|
|
27
|
+
comm: string,
|
|
28
|
+
adj: number | null,
|
|
29
|
+
anonKb = 409600,
|
|
30
|
+
) =>
|
|
31
|
+
`Memory cgroup out of memory: Killed process ${pid} (${comm}) total-vm:1263184kB, anon-rss:${anonKb}kB, file-rss:1024kB, shmem-rss:0kB, UID:0 pgtables:900kB${adj == null ? "" : ` oom_score_adj:${adj}`}`;
|
|
32
|
+
|
|
33
|
+
const entry = (time: number, msg: string): KernelLogEntry => ({ time, msg });
|
|
34
|
+
/** A kill with its header, as the kernel logs it when not rate-limited. */
|
|
35
|
+
const ourKill = (time: number, pid: number, adj = 1000): KernelLogEntry[] => [
|
|
36
|
+
entry(time - 0.00001, header(pid, "python3", OUR_ID)),
|
|
37
|
+
entry(time, killed(pid, "python3", adj)),
|
|
38
|
+
];
|
|
39
|
+
const theirKill = (time: number, pid: number): KernelLogEntry[] => [
|
|
40
|
+
entry(time - 0.00001, header(pid, "node", OTHER_ID)),
|
|
41
|
+
entry(time, killed(pid, "node", 0)),
|
|
42
|
+
];
|
|
43
|
+
/** A kill whose header the kernel rate-limited away. */
|
|
44
|
+
const bareKill = (time: number, pid: number): KernelLogEntry[] => [
|
|
45
|
+
entry(time, killed(pid, "python3", 1000)),
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
function sample(
|
|
49
|
+
oomKillDelta: number | null,
|
|
50
|
+
countersAvailable = true,
|
|
51
|
+
): ResourceSample {
|
|
52
|
+
const events = { low: 0, high: 0, max: 0, oom: 0, oomKill: 0 };
|
|
53
|
+
return {
|
|
54
|
+
ts: 0,
|
|
55
|
+
memory: {
|
|
56
|
+
currentBytes: 3_000_000_000,
|
|
57
|
+
limitBytes: 3_221_225_472,
|
|
58
|
+
peakBytes: 3_221_000_000,
|
|
59
|
+
ratio: 0.93,
|
|
60
|
+
},
|
|
61
|
+
memoryStat: null,
|
|
62
|
+
reclaim: null,
|
|
63
|
+
cpu: null,
|
|
64
|
+
events: countersAvailable ? events : null,
|
|
65
|
+
deltas:
|
|
66
|
+
oomKillDelta == null
|
|
67
|
+
? null
|
|
68
|
+
: {
|
|
69
|
+
events: { ...events, oomKill: oomKillDelta },
|
|
70
|
+
reclaim: null,
|
|
71
|
+
cpu: null,
|
|
72
|
+
},
|
|
73
|
+
disk: null,
|
|
74
|
+
activeConversations: null,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
describe("kernel log parsing", () => {
|
|
79
|
+
test("parses dmesg --json and plain dmesg into the same entries", () => {
|
|
80
|
+
const msg = killed(4242, "python3", 1000);
|
|
81
|
+
const json = `{"dmesg":[{"pri":3,"time": 44170.542000,"msg":${JSON.stringify(msg)}},{"pri":6,"time":44171.0,"msg":"x"}]}`;
|
|
82
|
+
const text = `[44170.542000] ${msg}\n[ 44171.000000] x\n`;
|
|
83
|
+
expect(parseDmesgJson(json)).toEqual(parseDmesgText(text));
|
|
84
|
+
expect(parseDmesgJson("not json")).toBeNull();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("pairs each kill with its header and tolerates missing headers and noise", () => {
|
|
88
|
+
const kills = oomKillsFromEntries([
|
|
89
|
+
entry(
|
|
90
|
+
30,
|
|
91
|
+
"bun invoked oom-killer: gfp_mask=0xcc0(GFP_KERNEL), order=0, oom_score_adj=-700",
|
|
92
|
+
),
|
|
93
|
+
...ourKill(31, 4242),
|
|
94
|
+
entry(32, "oom_reaper: reaped process 4242 (python3), now anon-rss:0kB"),
|
|
95
|
+
entry(50, killed(77, "node", null, 300000)),
|
|
96
|
+
]);
|
|
97
|
+
expect(kills).toEqual([
|
|
98
|
+
{
|
|
99
|
+
time: 31,
|
|
100
|
+
pid: 4242,
|
|
101
|
+
comm: "python3",
|
|
102
|
+
oomScoreAdj: 1000,
|
|
103
|
+
anonRssKb: 409600,
|
|
104
|
+
totalVmKb: 1263184,
|
|
105
|
+
memcg: memcg(OUR_ID),
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
time: 50,
|
|
109
|
+
pid: 77,
|
|
110
|
+
comm: "node",
|
|
111
|
+
oomScoreAdj: null,
|
|
112
|
+
anonRssKb: 300000,
|
|
113
|
+
totalVmKb: 1263184,
|
|
114
|
+
memcg: null,
|
|
115
|
+
},
|
|
116
|
+
]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("finds the container id in the runtime-named mounts only", () => {
|
|
120
|
+
const layer =
|
|
121
|
+
"aaaa000000000000000000000000000000000000000000000000000000000000";
|
|
122
|
+
const mountinfo = [
|
|
123
|
+
`151 137 0:34 /${OUR_ID}-1f3ead8083566ffb-hosts /etc/hosts rw,relatime - virtiofs kataShared rw`,
|
|
124
|
+
`152 139 0:34 /${OUR_ID}-175b29da4eda0e77-termination-log /dev/termination-log rw,relatime - virtiofs kataShared rw`,
|
|
125
|
+
`140 120 0:30 / / rw,relatime - overlay overlay rw,lowerdir=/var/lib/x/${layer}/fs`,
|
|
126
|
+
].join("\n");
|
|
127
|
+
expect(parseOwnContainerIds(mountinfo)).toEqual(new Set([OUR_ID]));
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("createOomKillReporter", () => {
|
|
132
|
+
let dir: string;
|
|
133
|
+
let recorded: Array<{ value?: number | null; detail?: unknown }>;
|
|
134
|
+
let reads: number;
|
|
135
|
+
let recordOk: boolean;
|
|
136
|
+
let consent: boolean | null;
|
|
137
|
+
let killCounter: number | null;
|
|
138
|
+
let entries: KernelLogEntry[] | null;
|
|
139
|
+
|
|
140
|
+
beforeEach(() => {
|
|
141
|
+
dir = mkdtempSync(join(tmpdir(), "oom-kill-reporter-"));
|
|
142
|
+
recorded = [];
|
|
143
|
+
reads = 0;
|
|
144
|
+
recordOk = true;
|
|
145
|
+
consent = true;
|
|
146
|
+
killCounter = 0;
|
|
147
|
+
entries = [];
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
afterEach(() => {
|
|
151
|
+
rmSync(dir, { recursive: true, force: true });
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
function reporter(bootId: string | null = "boot-a") {
|
|
155
|
+
return createOomKillReporter({
|
|
156
|
+
readKernelLog: async () => {
|
|
157
|
+
reads++;
|
|
158
|
+
return entries;
|
|
159
|
+
},
|
|
160
|
+
readKillCounter: () => killCounter,
|
|
161
|
+
readOwnContainerIds: () => new Set([OUR_ID]),
|
|
162
|
+
readBootId: () => bootId,
|
|
163
|
+
shareAnalytics: () => consent,
|
|
164
|
+
cursorPath: join(dir, "cursor.json"),
|
|
165
|
+
record: (record) => {
|
|
166
|
+
if (!recordOk) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
recorded.push(record);
|
|
170
|
+
return { id: "evt", createdAt: 0 };
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const pids = (report: { victims: { pid: number }[] } | null) =>
|
|
176
|
+
report?.victims.map((v) => v.pid) ?? [];
|
|
177
|
+
|
|
178
|
+
test("attributes by the header's cgroup regardless of order or count", async () => {
|
|
179
|
+
const r = reporter();
|
|
180
|
+
await r.check(sample(0), 0, 1);
|
|
181
|
+
|
|
182
|
+
entries = [...theirKill(70, 900), ...ourKill(80, 4242)];
|
|
183
|
+
killCounter = 1;
|
|
184
|
+
const report = await r.check(sample(1), 1_000, 1);
|
|
185
|
+
expect(report?.attribution).toBe("cgroup");
|
|
186
|
+
expect(pids(report)).toEqual([4242]);
|
|
187
|
+
expect(recorded[0].detail).toMatchObject({ killed_daemon: false });
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("uses the counter for header-less kills, oldest first; the rest are neighbours'", async () => {
|
|
191
|
+
const r = reporter();
|
|
192
|
+
await r.check(sample(0), 0, 1);
|
|
193
|
+
|
|
194
|
+
entries = [...bareKill(20, 1), ...bareKill(21, 2), ...bareKill(22, 3)];
|
|
195
|
+
killCounter = 2;
|
|
196
|
+
const first = await r.check(sample(2), 1_000, 1);
|
|
197
|
+
expect(first).toMatchObject({ attribution: "cgroup_counter", unnamed: 0 });
|
|
198
|
+
expect(pids(first)).toEqual([1, 2]);
|
|
199
|
+
|
|
200
|
+
// The third was logged but not counted, so it was another container's.
|
|
201
|
+
expect(await r.check(sample(0), 61_000, 1)).toBeNull();
|
|
202
|
+
expect(recorded).toHaveLength(1);
|
|
203
|
+
});
|
|
204
|
+
test("spends counter credit on the next scan and reports it unnamed only if unspent", async () => {
|
|
205
|
+
const r = reporter();
|
|
206
|
+
await r.check(sample(0), 0, 1);
|
|
207
|
+
|
|
208
|
+
// The kill is counted but its line is not yet visible: nothing to say yet.
|
|
209
|
+
killCounter = 1;
|
|
210
|
+
expect(await r.check(sample(1), 1_000, 1)).toBeNull();
|
|
211
|
+
|
|
212
|
+
entries = [...bareKill(5, 1)];
|
|
213
|
+
const spent = await r.check(sample(0), 61_000, 1);
|
|
214
|
+
expect(spent).toMatchObject({ attribution: "cgroup_counter", unnamed: 0 });
|
|
215
|
+
expect(pids(spent)).toEqual([1]);
|
|
216
|
+
|
|
217
|
+
// Credit that never finds a line is reported unnamed after one scan.
|
|
218
|
+
killCounter = 2;
|
|
219
|
+
expect(await r.check(sample(1), 62_000, 1)).toBeNull();
|
|
220
|
+
const expired = await r.check(sample(0), 123_000, 1);
|
|
221
|
+
expect(expired).toEqual({
|
|
222
|
+
attribution: "cgroup_counter",
|
|
223
|
+
victims: [],
|
|
224
|
+
unnamed: 1,
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
test("names every victim of a burst where the kernel rate-limits headers", async () => {
|
|
228
|
+
const r = reporter();
|
|
229
|
+
await r.check(sample(0), 0, 1);
|
|
230
|
+
|
|
231
|
+
const victims = [468, 472, 474, 477, 476, 467, 480, 471, 478, 473, 481];
|
|
232
|
+
const headerless = new Set([474, 468]);
|
|
233
|
+
const perKill = victims.map((pid, i) =>
|
|
234
|
+
headerless.has(pid)
|
|
235
|
+
? bareKill(216386 + i * 0.15, pid)
|
|
236
|
+
: ourKill(216386 + i * 0.15, pid),
|
|
237
|
+
);
|
|
238
|
+
// Kills visible in the log, then the counter read after them, per tick.
|
|
239
|
+
const ticks: Array<[number, number]> = [
|
|
240
|
+
[2, 2],
|
|
241
|
+
[5, 6],
|
|
242
|
+
[8, 8],
|
|
243
|
+
[11, 11],
|
|
244
|
+
];
|
|
245
|
+
const named: number[] = [];
|
|
246
|
+
let t = 1_000;
|
|
247
|
+
for (const [visible, counter] of ticks) {
|
|
248
|
+
entries = perKill.slice(0, visible).flat();
|
|
249
|
+
killCounter = counter;
|
|
250
|
+
const report = await r.check(sample(1), (t += 250), 1);
|
|
251
|
+
named.push(...pids(report));
|
|
252
|
+
}
|
|
253
|
+
expect([...named].sort((a, b) => a - b)).toEqual(
|
|
254
|
+
[...victims].sort((a, b) => a - b),
|
|
255
|
+
);
|
|
256
|
+
expect(recorded.reduce((n, e) => n + (e.value ?? 0), 0)).toBe(11);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("first scan reports the kill that preceded the monitor and flags the daemon by score", async () => {
|
|
260
|
+
entries = [...ourKill(5, 1, -700)];
|
|
261
|
+
const report = await reporter().check(sample(0), 0, 999);
|
|
262
|
+
expect(report?.attribution).toBe("kernel_log");
|
|
263
|
+
expect(recorded[0].detail).toMatchObject({
|
|
264
|
+
killed_daemon: true,
|
|
265
|
+
victim_count: 1,
|
|
266
|
+
memory_limit_bytes: 3_221_225_472,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// A second incarnation on the same kernel sees the same log: nothing new.
|
|
270
|
+
expect(await reporter().check(sample(0), 0, 999)).toBeNull();
|
|
271
|
+
expect(recorded).toHaveLength(1);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("first scan still excludes kills the header assigns elsewhere", async () => {
|
|
275
|
+
entries = [...theirKill(4, 900), ...ourKill(5, 1)];
|
|
276
|
+
expect(pids(await reporter().check(sample(0), 0, 1))).toEqual([1]);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("ignores header-less neighbours' kills when the local counter did not move", async () => {
|
|
280
|
+
const r = reporter();
|
|
281
|
+
await r.check(sample(0), 0, 1);
|
|
282
|
+
entries = [...bareKill(70, 900)];
|
|
283
|
+
expect(await r.check(sample(0), 61_000, 1)).toBeNull();
|
|
284
|
+
expect(recorded).toHaveLength(0);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("reports every new kill when there is no counter at all", async () => {
|
|
288
|
+
killCounter = null;
|
|
289
|
+
entries = [...bareKill(5, 1)];
|
|
290
|
+
await reporter().check(sample(null, false), 0, 1);
|
|
291
|
+
entries = [...bareKill(5, 1), ...bareKill(9, 2)];
|
|
292
|
+
const report = await reporter().check(sample(null, false), 61_000, 1);
|
|
293
|
+
expect(report?.attribution).toBe("kernel_log");
|
|
294
|
+
expect(pids(report)).toEqual([2]);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("skips a scan when a counter that exists fails to read", async () => {
|
|
298
|
+
const r = reporter();
|
|
299
|
+
await r.check(sample(0), 0, 1);
|
|
300
|
+
entries = [...bareKill(70, 900)];
|
|
301
|
+
killCounter = null;
|
|
302
|
+
expect(await r.check(sample(1), 1_000, 1)).toBeNull();
|
|
303
|
+
expect(recorded).toHaveLength(0);
|
|
304
|
+
// The scan was not consumed: the kill is still claimable once the counter reads.
|
|
305
|
+
killCounter = 1;
|
|
306
|
+
expect(pids(await r.check(sample(0), 1_250, 1))).toEqual([900]);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("a counter move with no readable log still produces an event, and does not double-count later", async () => {
|
|
310
|
+
entries = null;
|
|
311
|
+
killCounter = 3;
|
|
312
|
+
const r = reporter();
|
|
313
|
+
const report = await r.check(sample(3), 0, 1);
|
|
314
|
+
expect(report).toEqual({
|
|
315
|
+
attribution: "cgroup_counter",
|
|
316
|
+
victims: [],
|
|
317
|
+
unnamed: 3,
|
|
318
|
+
});
|
|
319
|
+
expect(recorded[0]).toMatchObject({ value: 3 });
|
|
320
|
+
|
|
321
|
+
entries = [...bareKill(1, 1), ...bareKill(2, 2), ...bareKill(3, 3)];
|
|
322
|
+
expect(await r.check(sample(0), 61_000, 1)).toBeNull();
|
|
323
|
+
expect(recorded).toHaveLength(1);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("a rebooted kernel resets the cursor even when the boot id is unreadable", async () => {
|
|
327
|
+
entries = [...bareKill(500, 1)];
|
|
328
|
+
await reporter(null).check(sample(0), 0, 1);
|
|
329
|
+
entries = [...bareKill(5, 1)];
|
|
330
|
+
const report = await reporter(null).check(sample(0), 0, 1);
|
|
331
|
+
expect(report?.victims).toHaveLength(1);
|
|
332
|
+
expect(recorded).toHaveLength(2);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("keeps the cursor and retries when the telemetry store refuses the event", async () => {
|
|
336
|
+
const r = reporter();
|
|
337
|
+
await r.check(sample(0), 0, 1);
|
|
338
|
+
recordOk = false;
|
|
339
|
+
entries = [...ourKill(5, 1)];
|
|
340
|
+
killCounter = 1;
|
|
341
|
+
expect(await r.check(sample(1), 1_000, 1)).not.toBeNull();
|
|
342
|
+
expect(recorded).toHaveLength(0);
|
|
343
|
+
|
|
344
|
+
recordOk = true;
|
|
345
|
+
await r.check(sample(0), 2_000, 1);
|
|
346
|
+
expect(recorded).toHaveLength(1);
|
|
347
|
+
|
|
348
|
+
// Durably acknowledged: a new incarnation does not repeat it.
|
|
349
|
+
expect(await reporter().check(sample(0), 0, 1)).toBeNull();
|
|
350
|
+
expect(recorded).toHaveLength(1);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test("an analytics opt-out advances the cursor without recording", async () => {
|
|
354
|
+
consent = false;
|
|
355
|
+
entries = [...ourKill(5, 1)];
|
|
356
|
+
await reporter().check(sample(0), 0, 1);
|
|
357
|
+
consent = true;
|
|
358
|
+
expect(await reporter().check(sample(0), 0, 1)).toBeNull();
|
|
359
|
+
expect(recorded).toHaveLength(0);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("scans on the first tick, on a counter move, and on the fallback cadence", async () => {
|
|
363
|
+
const r = reporter();
|
|
364
|
+
await r.check(sample(0), 0, 1);
|
|
365
|
+
await r.check(sample(0), 1_000, 1);
|
|
366
|
+
expect(reads).toBe(1);
|
|
367
|
+
await r.check(sample(1), 2_000, 1);
|
|
368
|
+
expect(reads).toBe(2);
|
|
369
|
+
await r.check(sample(0), 61_999, 1);
|
|
370
|
+
expect(reads).toBe(2);
|
|
371
|
+
await r.check(sample(0), 62_000, 1);
|
|
372
|
+
expect(reads).toBe(3);
|
|
373
|
+
});
|
|
374
|
+
});
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { getLogger } from "../util/logger.js";
|
|
12
|
+
import { MONITOR_OOM_SCORE_ADJ } from "../util/oom-priority.js";
|
|
12
13
|
import { getMonitoringPidPath } from "../util/platform.js";
|
|
13
14
|
import {
|
|
14
15
|
probeWorkerPidFile,
|
|
@@ -53,7 +54,7 @@ export async function spawnMonitoringWorkerProcess(
|
|
|
53
54
|
entry: MONITORING_WORKER_ENTRY,
|
|
54
55
|
packagedEntry: "monitoring",
|
|
55
56
|
workerLabel: "Resource monitor",
|
|
56
|
-
options: opts,
|
|
57
|
+
options: { ...opts, oomScoreAdj: MONITOR_OOM_SCORE_ADJ },
|
|
57
58
|
});
|
|
58
59
|
} catch (err) {
|
|
59
60
|
if (err instanceof WorkerProcessSpawnError) {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resets OOM protection that daemon children inherit at fork.
|
|
3
|
+
*
|
|
4
|
+
* The daemon runs at a strongly negative `oom_score_adj` and every spawn path
|
|
5
|
+
* is meant to reset its child, but a path that forgets leaves a child as
|
|
6
|
+
* protected as the daemon. The monitor sees every process, so it walks the
|
|
7
|
+
* daemon's descendants and writes 0 over any negative value it finds. Raising
|
|
8
|
+
* a score needs no capability. The write is logged so the missed spawn path
|
|
9
|
+
* can be fixed at its source.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
|
|
15
|
+
import { parseProcStat } from "./proc-wait-state.js";
|
|
16
|
+
|
|
17
|
+
export interface OomProtectionReset {
|
|
18
|
+
pid: number;
|
|
19
|
+
/** Kernel comm of the process; never the command line, which can carry secrets. */
|
|
20
|
+
comm: string;
|
|
21
|
+
previous: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function sweepInheritedOomProtection(options: {
|
|
25
|
+
daemonPid: number;
|
|
26
|
+
/** Descendants allowed to stay negative, such as the monitor itself. */
|
|
27
|
+
keepPids: ReadonlySet<number>;
|
|
28
|
+
procRoot?: string;
|
|
29
|
+
}): OomProtectionReset[] {
|
|
30
|
+
const procRoot = options.procRoot ?? "/proc";
|
|
31
|
+
let pids: number[];
|
|
32
|
+
try {
|
|
33
|
+
pids = readdirSync(procRoot)
|
|
34
|
+
.filter((entry) => /^\d+$/.test(entry))
|
|
35
|
+
.map(Number);
|
|
36
|
+
} catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const procs = new Map<number, { ppid: number; comm: string }>();
|
|
41
|
+
for (const pid of pids) {
|
|
42
|
+
try {
|
|
43
|
+
const stat = parseProcStat(
|
|
44
|
+
readFileSync(join(procRoot, String(pid), "stat"), "utf-8"),
|
|
45
|
+
);
|
|
46
|
+
if (stat) {
|
|
47
|
+
procs.set(pid, { ppid: stat.ppid, comm: stat.comm });
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
// Exited between readdir and read.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const descendsFromDaemon = (pid: number): boolean => {
|
|
55
|
+
const seen = new Set<number>();
|
|
56
|
+
for (
|
|
57
|
+
let cur = procs.get(pid)?.ppid;
|
|
58
|
+
cur != null && cur > 0 && !seen.has(cur);
|
|
59
|
+
cur = procs.get(cur)?.ppid
|
|
60
|
+
) {
|
|
61
|
+
if (cur === options.daemonPid) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
seen.add(cur);
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const resets: OomProtectionReset[] = [];
|
|
70
|
+
for (const [pid, info] of procs) {
|
|
71
|
+
if (
|
|
72
|
+
pid === options.daemonPid ||
|
|
73
|
+
options.keepPids.has(pid) ||
|
|
74
|
+
!descendsFromDaemon(pid)
|
|
75
|
+
) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const path = join(procRoot, String(pid), "oom_score_adj");
|
|
79
|
+
let previous: number;
|
|
80
|
+
try {
|
|
81
|
+
previous = parseInt(readFileSync(path, "utf-8"), 10);
|
|
82
|
+
} catch {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (!(previous < 0)) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
writeFileSync(path, "0");
|
|
90
|
+
resets.push({ pid, comm: info.comm, previous });
|
|
91
|
+
} catch {
|
|
92
|
+
// Exited between read and write.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return resets;
|
|
96
|
+
}
|