@vellumai/assistant 0.7.2 → 0.7.3
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/ARCHITECTURE.md +16 -1
- package/docs/architecture/memory.md +5 -2
- package/node_modules/@vellumai/gateway-client/src/ipc-client.ts +13 -4
- package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -9
- package/node_modules/@vellumai/slack-text/src/index.test.ts +18 -35
- package/node_modules/@vellumai/slack-text/src/index.ts +2 -48
- package/openapi.yaml +449 -22
- package/package.json +1 -1
- package/src/__tests__/app-control-flow.test.ts +21 -11
- package/src/__tests__/assistant-event-hub.test.ts +48 -0
- package/src/__tests__/assistant-event.test.ts +0 -10
- package/src/__tests__/assistant-events-sse-hardening.test.ts +2 -7
- package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -0
- package/src/__tests__/auto-analysis-end-to-end.test.ts +62 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +268 -0
- package/src/__tests__/call-conversation-messages.test.ts +8 -2
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +537 -0
- package/src/__tests__/channel-readiness-service.test.ts +4 -2
- package/src/__tests__/config-loader-backfill.test.ts +379 -0
- package/src/__tests__/config-schema.test.ts +1 -0
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +18 -9
- package/src/__tests__/config-watcher.test.ts +140 -69
- package/src/__tests__/context-search-agent-runner.test.ts +61 -3
- package/src/__tests__/context-search-conversations-source.test.ts +0 -24
- package/src/__tests__/context-search-fanout.test.ts +0 -1
- package/src/__tests__/context-search-memory-source.test.ts +3 -7
- package/src/__tests__/context-search-memory-v2-source.test.ts +0 -2
- package/src/__tests__/context-search-pkb-source.test.ts +0 -1
- package/src/__tests__/context-search-workspace-source.test.ts +0 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +6 -0
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +223 -0
- package/src/__tests__/conversation-agent-loop.test.ts +454 -5
- package/src/__tests__/conversation-error.test.ts +150 -3
- package/src/__tests__/conversation-process-callsite.test.ts +43 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +6 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +65 -0
- package/src/__tests__/conversation-slash-unknown.test.ts +6 -0
- package/src/__tests__/conversation-speed-override.test.ts +0 -3
- package/src/__tests__/conversation-store.test.ts +0 -18
- package/src/__tests__/conversation-surfaces-app-control.test.ts +15 -4
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +404 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +2 -5
- package/src/__tests__/conversation-workspace-injection.test.ts +6 -0
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +6 -0
- package/src/__tests__/credentials-cli.test.ts +7 -0
- package/src/__tests__/cu-unified-flow.test.ts +176 -10
- package/src/__tests__/date-context.test.ts +164 -2
- package/src/__tests__/disk-pressure-guard.test.ts +262 -0
- package/src/__tests__/disk-pressure-lifecycle.test.ts +168 -0
- package/src/__tests__/disk-pressure-policy.test.ts +241 -0
- package/src/__tests__/disk-pressure-routes.test.ts +379 -0
- package/src/__tests__/disk-pressure-tools.test.ts +277 -0
- package/src/__tests__/disk-usage.test.ts +150 -0
- package/src/__tests__/events-client-registration.test.ts +52 -0
- package/src/__tests__/events-dev-bypass-actor.test.ts +162 -0
- package/src/__tests__/file-write-tool.test.ts +4 -10
- package/src/__tests__/filing-service.test.ts +3 -4
- package/src/__tests__/heartbeat-disk-pressure.test.ts +183 -0
- package/src/__tests__/heartbeat-service.test.ts +260 -11
- package/src/__tests__/host-app-control-proxy.test.ts +195 -25
- package/src/__tests__/host-bash-proxy.test.ts +227 -34
- package/src/__tests__/host-bash-routes.test.ts +178 -13
- package/src/__tests__/host-cu-proxy.test.ts +210 -3
- package/src/__tests__/host-cu-routes-targeted.test.ts +141 -12
- package/src/__tests__/host-file-proxy-targeted.test.ts +48 -9
- package/src/__tests__/host-file-proxy.test.ts +268 -6
- package/src/__tests__/host-file-routes-targeted.test.ts +175 -17
- package/src/__tests__/host-transfer-proxy-targeted.test.ts +408 -59
- package/src/__tests__/host-transfer-routes-targeted.test.ts +232 -17
- package/src/__tests__/http-user-message-parity.test.ts +107 -1
- package/src/__tests__/injector-chain.test.ts +18 -6
- package/src/__tests__/injector-disk-pressure.test.ts +224 -0
- package/src/__tests__/managed-profile-guard.test.ts +18 -0
- package/src/__tests__/mcp-abort-signal.test.ts +130 -0
- package/src/__tests__/memory-admin-recall.test.ts +3 -11
- package/src/__tests__/memory-retrieval-pipeline.test.ts +22 -1
- package/src/__tests__/normalize-onboarding.test.ts +180 -0
- package/src/__tests__/oauth-connect-routes.test.ts +316 -0
- package/src/__tests__/oauth-provider-seed-logos.test.ts +24 -2
- package/src/__tests__/onboarding-persona-write.test.ts +308 -0
- package/src/__tests__/openai-provider.test.ts +45 -8
- package/src/__tests__/persist-onboarding-artifacts.test.ts +44 -64
- package/src/__tests__/platform-callback-registration.test.ts +21 -4
- package/src/__tests__/platform.test.ts +2 -1
- package/src/__tests__/playbook-execution.test.ts +0 -43
- package/src/__tests__/plugin-tool-contribution.test.ts +47 -0
- package/src/__tests__/prechat-onboarding-contract.test.ts +214 -27
- package/src/__tests__/provider-tool-name.test.ts +23 -0
- package/src/__tests__/relay-server.test.ts +15 -4
- package/src/__tests__/runtime-events-sse.test.ts +4 -8
- package/src/__tests__/scheduler-disk-pressure.test.ts +148 -0
- package/src/__tests__/secret-ingress-http.test.ts +0 -1
- package/src/__tests__/suggestion-routes.test.ts +46 -0
- package/src/__tests__/twilio-validation.test.ts +2 -2
- package/src/__tests__/workspace-migration-065-bump-stale-heartbeat-interval.test.ts +122 -0
- package/src/__tests__/workspace-migration-066-seed-heartbeat-callsite-cost-default.test.ts +285 -0
- package/src/__tests__/workspace-migration-068-release-notes-local-timezone.test.ts +90 -0
- package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +90 -0
- package/src/approvals/guardian-decision-primitive.ts +13 -0
- package/src/approvals/guardian-request-resolvers.ts +16 -17
- package/src/backup/snapshot-lock.ts +2 -27
- package/src/bundler/compiler-tools.ts +3 -2
- package/src/calls/call-conversation-messages.ts +46 -10
- package/src/cli/commands/__tests__/webhooks.test.ts +0 -4
- package/src/cli/commands/bash.ts +35 -108
- package/src/cli/commands/contacts.ts +64 -25
- package/src/cli/commands/credentials.ts +56 -0
- package/src/cli/commands/memory-v2.ts +7 -6
- package/src/cli/commands/oauth/__tests__/connect.test.ts +437 -1
- package/src/cli/commands/oauth/connect.ts +127 -1
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +0 -3
- package/src/cli/commands/platform/__tests__/connect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +7 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +103 -6
- package/src/cli/commands/platform/index.ts +16 -7
- package/src/cli/commands/status.ts +57 -0
- package/src/cli/program.ts +4 -2
- package/src/config/assistant-feature-flags.ts +13 -3
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +4 -3
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +13 -7
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +2 -2
- package/src/config/env.ts +0 -8
- package/src/config/feature-flag-registry.json +27 -3
- package/src/config/loader.ts +127 -8
- package/src/config/schemas/__tests__/memory-v2.test.ts +10 -5
- package/src/config/schemas/call-site-catalog.ts +14 -0
- package/src/config/schemas/channels.ts +0 -5
- package/src/config/schemas/heartbeat.ts +1 -1
- package/src/config/schemas/llm.ts +2 -0
- package/src/config/schemas/memory-lifecycle.ts +13 -0
- package/src/config/schemas/memory-v2.ts +75 -11
- package/src/config/schemas/platform.ts +43 -3
- package/src/config/schemas/services.ts +28 -0
- package/src/config/seed-inference-profiles.ts +230 -33
- package/src/contacts/contact-store.ts +0 -25
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +86 -25
- package/src/daemon/assistant-attachments.ts +4 -4
- package/src/daemon/config-watcher.ts +85 -57
- package/src/daemon/conversation-agent-loop-handlers.ts +6 -0
- package/src/daemon/conversation-agent-loop.ts +170 -33
- package/src/daemon/conversation-error.ts +87 -15
- package/src/daemon/conversation-lifecycle.ts +1 -3
- package/src/daemon/conversation-process.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +26 -0
- package/src/daemon/conversation-store.ts +2 -2
- package/src/daemon/conversation-surfaces.ts +195 -15
- package/src/daemon/conversation-tool-setup.ts +57 -14
- package/src/daemon/conversation.ts +17 -22
- package/src/daemon/date-context.ts +71 -22
- package/src/daemon/disk-pressure-background-gate.ts +73 -0
- package/src/daemon/disk-pressure-guard.ts +343 -0
- package/src/daemon/disk-pressure-policy.ts +163 -0
- package/src/daemon/handlers/shared.ts +0 -1
- package/src/daemon/handlers/skills.ts +3 -4
- package/src/daemon/host-app-control-proxy.ts +137 -41
- package/src/daemon/host-bash-proxy.ts +46 -21
- package/src/daemon/host-cu-proxy.ts +49 -3
- package/src/daemon/host-file-proxy.ts +43 -7
- package/src/daemon/host-transfer-proxy.ts +95 -4
- package/src/daemon/lifecycle.ts +79 -28
- package/src/daemon/meet-host-supervisor.ts +4 -4
- package/src/daemon/meet-manifest-loader.ts +0 -1
- package/src/daemon/memory-v2-startup.ts +14 -4
- package/src/daemon/message-protocol.ts +3 -0
- package/src/daemon/message-types/conversations.ts +4 -0
- package/src/daemon/message-types/disk-pressure.ts +9 -0
- package/src/daemon/message-types/messages.ts +3 -0
- package/src/daemon/profiler-run-store.ts +5 -5
- package/src/daemon/tool-setup-types.ts +2 -2
- package/src/documents/document-store.ts +85 -0
- package/src/filing/filing-service.ts +30 -5
- package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +9 -16
- package/src/heartbeat/__tests__/heartbeat-run-store.test.ts +36 -0
- package/src/heartbeat/heartbeat-run-store.ts +13 -0
- package/src/heartbeat/heartbeat-service.ts +205 -31
- package/src/home/feed-scheduler.ts +18 -0
- package/src/inbound/platform-callback-registration.ts +8 -15
- package/src/ipc/__tests__/clients-list-ipc.test.ts +169 -0
- package/src/ipc/assistant-server.ts +56 -2
- package/src/ipc/gateway-client.ts +37 -3
- package/src/live-voice/live-voice-archive.ts +4 -4
- package/src/live-voice/protocol.ts +5 -7
- package/src/media/image-service.ts +1 -7
- package/src/memory/__tests__/fixtures/memory-v2-activation-fixtures.ts +21 -13
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +52 -22
- package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +0 -6
- package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +272 -0
- package/src/memory/admin.ts +5 -9
- package/src/memory/context-search/agent-runner.ts +19 -2
- package/src/memory/context-search/sources/conversations.ts +2 -11
- package/src/memory/context-search/sources/memory-v2.ts +5 -4
- package/src/memory/context-search/sources/memory.ts +0 -1
- package/src/memory/context-search/types.ts +0 -1
- package/src/memory/conversation-crud.ts +4 -12
- package/src/memory/db-init.ts +2 -0
- package/src/memory/embedding-runtime-manager.ts +119 -5
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +32 -21
- package/src/memory/graph/conversation-graph-memory.ts +42 -54
- package/src/memory/graph/extraction.ts +1 -3
- package/src/memory/graph/graph-search.test.ts +10 -67
- package/src/memory/graph/graph-search.ts +1 -20
- package/src/memory/graph/retriever.test.ts +6 -0
- package/src/memory/graph/retriever.ts +6 -10
- package/src/memory/indexer.ts +54 -45
- package/src/memory/job-handlers/backfill.ts +2 -11
- package/src/memory/job-handlers/cleanup.ts +43 -0
- package/src/memory/job-handlers/embedding.ts +6 -8
- package/src/memory/job-handlers/summarization.ts +2 -7
- package/src/memory/jobs-store.ts +48 -0
- package/src/memory/jobs-worker.ts +81 -43
- package/src/memory/memory-v2-activation-log-store.ts +32 -14
- package/src/memory/memory-v2-concept-frequency.ts +169 -0
- package/src/memory/migrations/239-trace-events-created-at-index.ts +18 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/pkb/pkb-search.test.ts +6 -0
- package/src/memory/qdrant-client.ts +0 -13
- package/src/memory/rerank-local.ts +374 -0
- package/src/memory/search/semantic.ts +6 -67
- package/src/memory/trace-event-store.ts +1 -17
- package/src/memory/v2/__tests__/activation.test.ts +311 -250
- package/src/memory/v2/__tests__/consolidation-job.test.ts +40 -8
- package/src/memory/v2/__tests__/injection.test.ts +157 -167
- package/src/memory/v2/__tests__/prompts-consolidation.test.ts +61 -2
- package/src/memory/v2/__tests__/qdrant.test.ts +16 -0
- package/src/memory/v2/__tests__/reranker.test.ts +338 -0
- package/src/memory/v2/__tests__/sim.test.ts +5 -199
- package/src/memory/v2/__tests__/skill-store.test.ts +71 -65
- package/src/memory/v2/__tests__/static-context.test.ts +76 -1
- package/src/memory/v2/activation.ts +149 -156
- package/src/memory/v2/consolidation-job.ts +62 -12
- package/src/memory/v2/injection.ts +47 -60
- package/src/memory/v2/prompts/consolidation.ts +36 -1
- package/src/memory/v2/qdrant.ts +99 -0
- package/src/memory/v2/reranker.ts +177 -0
- package/src/memory/v2/sim.ts +10 -84
- package/src/memory/v2/skill-content.ts +4 -3
- package/src/memory/v2/skill-store.ts +82 -59
- package/src/memory/v2/static-context.ts +22 -0
- package/src/memory/v2/types.ts +10 -10
- package/src/notifications/copy-composer.ts +13 -0
- package/src/notifications/signal.ts +4 -0
- package/src/oauth/AGENTS.md +3 -1
- package/src/oauth/__tests__/oauth-connect-state.test.ts +137 -0
- package/src/oauth/connect-orchestrator.ts +2 -0
- package/src/oauth/connection-resolver.test.ts +66 -1
- package/src/oauth/connection-resolver.ts +55 -1
- package/src/oauth/oauth-connect-state.ts +77 -0
- package/src/oauth/seed-providers.ts +58 -1
- package/src/plugins/defaults/injectors.ts +35 -2
- package/src/plugins/defaults/memory-retrieval.ts +5 -6
- package/src/plugins/types.ts +7 -0
- package/src/proactive-artifact/aux-message-injector.ts +74 -0
- package/src/proactive-artifact/decision.test.ts +226 -0
- package/src/proactive-artifact/decision.ts +165 -0
- package/src/proactive-artifact/index.ts +7 -0
- package/src/proactive-artifact/job.test.ts +867 -0
- package/src/proactive-artifact/job.ts +352 -0
- package/src/proactive-artifact/message-copy.ts +41 -0
- package/src/proactive-artifact/trigger-state.test.ts +277 -0
- package/src/proactive-artifact/trigger-state.ts +119 -0
- package/src/prompts/normalize-onboarding.ts +80 -0
- package/src/prompts/persona-resolver.ts +101 -9
- package/src/prompts/system-prompt.ts +21 -7
- package/src/prompts/templates/BOOTSTRAP.md +13 -5
- package/src/providers/__tests__/retry-callsite.test.ts +222 -1
- package/src/providers/model-intents.ts +7 -0
- package/src/providers/openrouter/client.ts +8 -0
- package/src/providers/retry.ts +50 -0
- package/src/providers/types.ts +1 -0
- package/src/runtime/__tests__/agent-wake.test.ts +456 -3
- package/src/runtime/agent-wake.ts +238 -100
- package/src/runtime/assistant-event-hub.ts +36 -6
- package/src/runtime/assistant-event.ts +0 -1
- package/src/runtime/auth/__tests__/route-policy.test.ts +64 -0
- package/src/runtime/auth/route-policy.ts +14 -1
- package/src/runtime/auth/same-actor.ts +216 -0
- package/src/runtime/channel-retry-sweep.ts +65 -1
- package/src/runtime/guardian-reply-router.ts +10 -0
- package/src/runtime/local-actor-identity.ts +52 -11
- package/src/runtime/pending-interactions.ts +8 -0
- package/src/runtime/routes/__tests__/client-routes.test.ts +155 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +0 -5
- package/src/runtime/routes/__tests__/heartbeat-routes.test.ts +1 -1
- package/src/runtime/routes/client-routes.ts +20 -2
- package/src/runtime/routes/contact-routes.ts +0 -25
- package/src/runtime/routes/conversation-routes.ts +35 -26
- package/src/runtime/routes/debug-bash-routes.ts +163 -0
- package/src/runtime/routes/disk-pressure-routes.ts +121 -0
- package/src/runtime/routes/document-pdf-renderer.ts +6 -2
- package/src/runtime/routes/documents-routes.ts +2 -75
- package/src/runtime/routes/events-routes.ts +41 -9
- package/src/runtime/routes/host-bash-routes.ts +23 -3
- package/src/runtime/routes/host-cu-routes.ts +33 -6
- package/src/runtime/routes/host-file-routes.ts +32 -6
- package/src/runtime/routes/host-transfer-routes.ts +79 -16
- package/src/runtime/routes/identity-routes.ts +7 -138
- package/src/runtime/routes/inbound-message-handler.ts +77 -12
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -0
- package/src/runtime/routes/index.ts +6 -0
- package/src/runtime/routes/memory-item-routes.test.ts +41 -15
- package/src/runtime/routes/memory-v2-routes.ts +33 -0
- package/src/runtime/routes/oauth-connect-routes.ts +153 -0
- package/src/runtime/verification-outbound-actions.ts +4 -4
- package/src/schedule/run-script.ts +37 -5
- package/src/schedule/scheduler.ts +20 -1
- package/src/security/encrypted-store.ts +2 -0
- package/src/security/secure-keys.ts +55 -0
- package/src/skills/remote-skill-policy.ts +4 -10
- package/src/subagent/index.ts +1 -7
- package/src/subagent/manager.ts +1 -15
- package/src/tasks/task-runner.ts +0 -1
- package/src/tasks/task-store.ts +0 -3
- package/src/tools/background-tool-registry.ts +17 -3
- package/src/tools/host-filesystem/edit.test.ts +151 -0
- package/src/tools/host-filesystem/edit.ts +43 -1
- package/src/tools/host-filesystem/read.test.ts +129 -0
- package/src/tools/host-filesystem/read.ts +43 -1
- package/src/tools/host-filesystem/transfer.test.ts +127 -2
- package/src/tools/host-filesystem/transfer.ts +56 -11
- package/src/tools/host-filesystem/write.test.ts +134 -0
- package/src/tools/host-filesystem/write.ts +43 -1
- package/src/tools/host-terminal/host-shell.ts +13 -6
- package/src/tools/mcp/mcp-tool-factory.ts +2 -1
- package/src/tools/memory/register.test.ts +12 -9
- package/src/tools/memory/register.ts +1 -2
- package/src/tools/provider-tool-name.ts +28 -0
- package/src/tools/registry.ts +30 -9
- package/src/tools/terminal/shell.ts +9 -1
- package/src/tools/tool-approval-handler.ts +31 -6
- package/src/tools/types.ts +24 -2
- package/src/tts/provider-catalog.ts +3 -5
- package/src/util/disk-usage.ts +138 -0
- package/src/util/platform.ts +21 -11
- package/src/util/process-liveness.ts +26 -0
- package/src/workspace/heartbeat-service.ts +19 -0
- package/src/workspace/migrations/065-bump-stale-heartbeat-interval.ts +60 -0
- package/src/workspace/migrations/066-seed-heartbeat-callsite-cost-default.ts +146 -0
- package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +72 -0
- package/src/workspace/migrations/068-release-notes-local-timezone.ts +65 -0
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/__tests__/conversation-tool-setup-memory-scope.test.ts +0 -167
- package/src/memory/v2/__tests__/skill-qdrant.test.ts +0 -657
- package/src/memory/v2/skill-qdrant.ts +0 -404
- package/src/signals/bash.ts +0 -198
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
// Memory v2 — Per-turn activation update
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
4
|
//
|
|
5
|
-
// Implements the activation formula from §4 of the design doc
|
|
5
|
+
// Implements the activation formula from §4 of the design doc plus an
|
|
6
|
+
// additive cross-encoder rerank boost on the unified top-K-by-A_o pool:
|
|
6
7
|
//
|
|
7
8
|
// A_o(n, t+1) = d · A(n, t)
|
|
8
9
|
// + c_user · sim(User_{t+1}, n)
|
|
9
10
|
// + c_assistant · sim(Assistant_t, n)
|
|
10
11
|
// + c_now · sim(NOW.md, n)
|
|
12
|
+
// + c_user · α · r_norm(User_{t+1}, n) [n ∈ topK]
|
|
13
|
+
// + c_assistant · α · r_norm(Assistant_t, n) [n ∈ topK]
|
|
11
14
|
//
|
|
12
15
|
// A(n, t+1) = [ A_o(n)
|
|
13
16
|
// + k · Σ_{m∈in1(n)} A_o(m)
|
|
@@ -40,7 +43,8 @@ import {
|
|
|
40
43
|
import { clampUnitInterval } from "../validation.js";
|
|
41
44
|
import type { EdgeIndex } from "./edge-index.js";
|
|
42
45
|
import { hybridQueryConceptPages } from "./qdrant.js";
|
|
43
|
-
import {
|
|
46
|
+
import { rerankCandidates } from "./reranker.js";
|
|
47
|
+
import { simBatch } from "./sim.js";
|
|
44
48
|
import type { ActivationState, EverInjectedEntry } from "./types.js";
|
|
45
49
|
|
|
46
50
|
/**
|
|
@@ -77,6 +81,7 @@ interface SelectCandidatesParams {
|
|
|
77
81
|
/** NOW context string (essentials/threads/recent or NOW.md). */
|
|
78
82
|
nowText: string;
|
|
79
83
|
config: AssistantConfig;
|
|
84
|
+
signal?: AbortSignal;
|
|
80
85
|
}
|
|
81
86
|
|
|
82
87
|
interface SelectCandidatesResult {
|
|
@@ -104,7 +109,8 @@ interface SelectCandidatesResult {
|
|
|
104
109
|
export async function selectCandidates(
|
|
105
110
|
params: SelectCandidatesParams,
|
|
106
111
|
): Promise<SelectCandidatesResult> {
|
|
107
|
-
const { priorState, userText, assistantText, nowText, config } =
|
|
112
|
+
const { priorState, userText, assistantText, nowText, config, signal } =
|
|
113
|
+
params;
|
|
108
114
|
|
|
109
115
|
const fromPrior = new Set<string>();
|
|
110
116
|
const fromAnn = new Set<string>();
|
|
@@ -125,12 +131,16 @@ export async function selectCandidates(
|
|
|
125
131
|
.join("\n");
|
|
126
132
|
|
|
127
133
|
if (annQueryText.length > 0) {
|
|
128
|
-
|
|
134
|
+
throwIfAborted(signal);
|
|
135
|
+
const denseResult = await embedWithBackend(config, [annQueryText], {
|
|
136
|
+
signal,
|
|
137
|
+
});
|
|
129
138
|
const dense = await applyCorrectionIfCalibrated(
|
|
130
139
|
denseResult.vectors[0],
|
|
131
140
|
denseResult.provider,
|
|
132
141
|
denseResult.model,
|
|
133
142
|
);
|
|
143
|
+
throwIfAborted(signal);
|
|
134
144
|
const sparse = generateSparseEmbedding(annQueryText);
|
|
135
145
|
const limit =
|
|
136
146
|
config.memory.v2.ann_candidate_limit ?? UNLIMITED_ANN_CANDIDATE_LIMIT;
|
|
@@ -154,6 +164,7 @@ interface ComputeOwnActivationParams {
|
|
|
154
164
|
assistantText: string;
|
|
155
165
|
nowText: string;
|
|
156
166
|
config: AssistantConfig;
|
|
167
|
+
signal?: AbortSignal;
|
|
157
168
|
}
|
|
158
169
|
|
|
159
170
|
/**
|
|
@@ -164,12 +175,18 @@ interface ComputeOwnActivationParams {
|
|
|
164
175
|
interface OwnActivationBreakdown {
|
|
165
176
|
/** `d * prev(slug)` — the decayed prior-turn activation contribution. */
|
|
166
177
|
priorContribution: number;
|
|
167
|
-
/** Raw `sim(user, slug)
|
|
178
|
+
/** Raw fused `sim(user, slug)`, before `c_user` weighting. */
|
|
168
179
|
simUser: number;
|
|
169
|
-
/** Raw `sim(assistant, slug)
|
|
180
|
+
/** Raw fused `sim(assistant, slug)`, before `c_assistant` weighting. */
|
|
170
181
|
simAssistant: number;
|
|
171
|
-
/** Raw `sim(now, slug)
|
|
182
|
+
/** Raw fused `sim(now, slug)`, before `c_now` weighting. */
|
|
172
183
|
simNow: number;
|
|
184
|
+
/** Rerank delta `α · r_norm_u`; 0 outside the top-K pool. Applied to `A_o` weighted by `c_user`. */
|
|
185
|
+
simUserRerankBoost: number;
|
|
186
|
+
/** Rerank delta `α · r_norm_a`; 0 outside the top-K pool. Applied to `A_o` weighted by `c_assistant`. NOW skips rerank. */
|
|
187
|
+
simAssistantRerankBoost: number;
|
|
188
|
+
/** True when this slug was in the unified top-K rerank pool. Lets the inspector distinguish "cross-encoder normalised to 0" from "rerank skipped this slug." */
|
|
189
|
+
inRerankPool: boolean;
|
|
173
190
|
}
|
|
174
191
|
|
|
175
192
|
interface ComputeOwnActivationResult {
|
|
@@ -181,21 +198,34 @@ interface ComputeOwnActivationResult {
|
|
|
181
198
|
|
|
182
199
|
/**
|
|
183
200
|
* Apply the own-activation formula
|
|
184
|
-
* A_o(n) = d · prev(n)
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
* candidate set
|
|
188
|
-
*
|
|
189
|
-
*
|
|
201
|
+
* A_o(n) = d · prev(n)
|
|
202
|
+
* + c_user · sim_u + c_assistant · sim_a + c_now · sim_n
|
|
203
|
+
* + c_user · α · r_norm_u + c_assistant · α · r_norm_a
|
|
204
|
+
* over the candidate set, where the rerank terms only fire for slugs that
|
|
205
|
+
* land in the unified top-K-by-pre-rerank-A_o window. Returns a sparse map
|
|
206
|
+
* keyed by slug; slugs whose computed value rounds to 0 are still included
|
|
207
|
+
* so callers can see the candidate set explicitly. Also returns a per-slug
|
|
208
|
+
* breakdown of the raw inputs (decayed prior + raw sims + rerank deltas) so
|
|
209
|
+
* callers can render contribution diagnostics without re-running the math.
|
|
190
210
|
*
|
|
191
211
|
* The three `simBatch` calls run concurrently — they hit independent named
|
|
192
|
-
* vectors and embed independent query texts.
|
|
212
|
+
* vectors and embed independent query texts. Cross-encoder rerank then runs
|
|
213
|
+
* once on the unified top-K (selected by pre-rerank A_o, not per-channel
|
|
214
|
+
* fused sim) so an entry strong in both channels can't double-boost itself
|
|
215
|
+
* past entries that only land in one channel.
|
|
193
216
|
*/
|
|
194
217
|
export async function computeOwnActivation(
|
|
195
218
|
params: ComputeOwnActivationParams,
|
|
196
219
|
): Promise<ComputeOwnActivationResult> {
|
|
197
|
-
const {
|
|
198
|
-
|
|
220
|
+
const {
|
|
221
|
+
candidates,
|
|
222
|
+
priorState,
|
|
223
|
+
userText,
|
|
224
|
+
assistantText,
|
|
225
|
+
nowText,
|
|
226
|
+
config,
|
|
227
|
+
signal,
|
|
228
|
+
} = params;
|
|
199
229
|
|
|
200
230
|
const activation = new Map<string, number>();
|
|
201
231
|
const breakdown = new Map<string, OwnActivationBreakdown>();
|
|
@@ -204,30 +234,122 @@ export async function computeOwnActivation(
|
|
|
204
234
|
const { d, c_user, c_assistant, c_now } = config.memory.v2;
|
|
205
235
|
const slugList = [...candidates];
|
|
206
236
|
|
|
237
|
+
// NOW context is structured (timestamps, current focus) — outside the
|
|
238
|
+
// cross-encoder's training distribution, so it never participates in rerank.
|
|
207
239
|
const [simUser, simAssistant, simNow] = await Promise.all([
|
|
208
|
-
simBatch(userText, slugList, config),
|
|
209
|
-
simBatch(assistantText, slugList, config),
|
|
210
|
-
simBatch(nowText, slugList, config),
|
|
240
|
+
simBatch(userText, slugList, config, { signal }),
|
|
241
|
+
simBatch(assistantText, slugList, config, { signal }),
|
|
242
|
+
simBatch(nowText, slugList, config, { signal }),
|
|
211
243
|
]);
|
|
212
244
|
|
|
213
|
-
|
|
245
|
+
interface SlugInputs {
|
|
246
|
+
slug: string;
|
|
247
|
+
priorContribution: number;
|
|
248
|
+
simU: number;
|
|
249
|
+
simA: number;
|
|
250
|
+
simN: number;
|
|
251
|
+
/** Pre-rerank A_o; ranking signal for the unified rerank pool. */
|
|
252
|
+
preRerank: number;
|
|
253
|
+
}
|
|
254
|
+
const inputs: SlugInputs[] = slugList.map((slug) => {
|
|
214
255
|
const prev = priorState?.state[slug] ?? 0;
|
|
215
256
|
const simU = simUser.get(slug) ?? 0;
|
|
216
257
|
const simA = simAssistant.get(slug) ?? 0;
|
|
217
258
|
const simN = simNow.get(slug) ?? 0;
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
priorContribution
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
259
|
+
const priorContribution = d * prev;
|
|
260
|
+
return {
|
|
261
|
+
slug,
|
|
262
|
+
priorContribution,
|
|
263
|
+
simU,
|
|
264
|
+
simA,
|
|
265
|
+
simN,
|
|
266
|
+
preRerank:
|
|
267
|
+
priorContribution + c_user * simU + c_assistant * simA + c_now * simN,
|
|
268
|
+
};
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// Unified top-K by pre-rerank A_o. Both channels rerank against the **same**
|
|
272
|
+
// slug set, so a slug strong on user can't crowd out one strong on assistant
|
|
273
|
+
// by virtue of appearing in both per-channel top-Ks. Both channel queries
|
|
274
|
+
// ride in a single `rerankCandidates` call so the worker tokenizes and
|
|
275
|
+
// forward-passes them together — half the per-call overhead of two
|
|
276
|
+
// serialised round-trips.
|
|
277
|
+
let userRerankBoost: ReadonlyMap<string, number> = new Map();
|
|
278
|
+
let assistantRerankBoost: ReadonlyMap<string, number> = new Map();
|
|
279
|
+
let inPoolSet: ReadonlySet<string> = new Set();
|
|
280
|
+
const rerankCfg = config.memory.v2.rerank;
|
|
281
|
+
if (rerankCfg?.enabled) {
|
|
282
|
+
throwIfAborted(signal);
|
|
283
|
+
const topSlugs = inputs
|
|
284
|
+
.slice()
|
|
285
|
+
.sort((a, b) => b.preRerank - a.preRerank)
|
|
286
|
+
.slice(0, rerankCfg.top_k)
|
|
287
|
+
.map((e) => e.slug);
|
|
288
|
+
if (topSlugs.length > 0) {
|
|
289
|
+
inPoolSet = new Set(topSlugs);
|
|
290
|
+
const [userScores, assistantScores] = await rerankCandidates(
|
|
291
|
+
[userText, assistantText],
|
|
292
|
+
topSlugs,
|
|
293
|
+
config,
|
|
294
|
+
);
|
|
295
|
+
throwIfAborted(signal);
|
|
296
|
+
userRerankBoost = normalizeRerankScores(userScores, rerankCfg.alpha);
|
|
297
|
+
assistantRerankBoost = normalizeRerankScores(
|
|
298
|
+
assistantScores,
|
|
299
|
+
rerankCfg.alpha,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
for (const e of inputs) {
|
|
305
|
+
const boostU = userRerankBoost.get(e.slug) ?? 0;
|
|
306
|
+
const boostA = assistantRerankBoost.get(e.slug) ?? 0;
|
|
307
|
+
activation.set(
|
|
308
|
+
e.slug,
|
|
309
|
+
clampUnitInterval(e.preRerank + c_user * boostU + c_assistant * boostA),
|
|
310
|
+
);
|
|
311
|
+
breakdown.set(e.slug, {
|
|
312
|
+
priorContribution: e.priorContribution,
|
|
313
|
+
simUser: e.simU,
|
|
314
|
+
simAssistant: e.simA,
|
|
315
|
+
simNow: e.simN,
|
|
316
|
+
simUserRerankBoost: boostU,
|
|
317
|
+
simAssistantRerankBoost: boostA,
|
|
318
|
+
inRerankPool: inPoolSet.has(e.slug),
|
|
225
319
|
});
|
|
226
320
|
}
|
|
227
321
|
|
|
228
322
|
return { activation, breakdown };
|
|
229
323
|
}
|
|
230
324
|
|
|
325
|
+
/**
|
|
326
|
+
* Per-batch normalisation: divide raw cross-encoder scores by the channel's
|
|
327
|
+
* own max and return `alpha · r_norm` per slug. Empty input or all-zero
|
|
328
|
+
* scores yield an empty Map so the channel contributes 0 boost.
|
|
329
|
+
*/
|
|
330
|
+
function normalizeRerankScores(
|
|
331
|
+
rawScores: ReadonlyMap<string, number>,
|
|
332
|
+
alpha: number,
|
|
333
|
+
): Map<string, number> {
|
|
334
|
+
const out = new Map<string, number>();
|
|
335
|
+
if (rawScores.size === 0) return out;
|
|
336
|
+
let maxScore = 0;
|
|
337
|
+
for (const v of rawScores.values()) {
|
|
338
|
+
if (v > maxScore) maxScore = v;
|
|
339
|
+
}
|
|
340
|
+
if (maxScore === 0) return out;
|
|
341
|
+
for (const [slug, raw] of rawScores) {
|
|
342
|
+
out.set(slug, alpha * (raw / maxScore));
|
|
343
|
+
}
|
|
344
|
+
return out;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function throwIfAborted(signal: AbortSignal | undefined): void {
|
|
348
|
+
if (signal?.aborted) {
|
|
349
|
+
throw new DOMException("Aborted", "AbortError");
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
231
353
|
// ---------------------------------------------------------------------------
|
|
232
354
|
// Spreading activation
|
|
233
355
|
// ---------------------------------------------------------------------------
|
|
@@ -411,132 +533,3 @@ export function selectInjections(
|
|
|
411
533
|
|
|
412
534
|
return { topNow, toInject };
|
|
413
535
|
}
|
|
414
|
-
|
|
415
|
-
// ---------------------------------------------------------------------------
|
|
416
|
-
// Skill autoinjection — candidate / activation / injection selection
|
|
417
|
-
// ---------------------------------------------------------------------------
|
|
418
|
-
//
|
|
419
|
-
// Skills are stateless: there is no decay carry-over (`d · prev`), no
|
|
420
|
-
// spreading activation, and no `everInjected` dedup. The agent re-presents
|
|
421
|
-
// the top-K active skills every turn so it can drop or pick them up freely.
|
|
422
|
-
// The pipeline therefore reduces to:
|
|
423
|
-
// 1. ANN candidate selection against the dedicated skills collection.
|
|
424
|
-
// 2. Pure similarity-only activation: A_skill = c_user·sim_u +
|
|
425
|
-
// c_assistant·sim_a + c_now·sim_n, clamped to [0, 1].
|
|
426
|
-
// 3. Top-K by activation, lexicographic tie-break, no injection delta.
|
|
427
|
-
//
|
|
428
|
-
// The activation coefficients are reused from `config.memory.v2.{c_user,
|
|
429
|
-
// c_assistant, c_now}` — the design doc (§9) deliberately shares them with
|
|
430
|
-
// concept-page activation rather than introducing parallel knobs.
|
|
431
|
-
|
|
432
|
-
interface ComputeSkillActivationParams {
|
|
433
|
-
candidates: ReadonlySet<string>;
|
|
434
|
-
userText: string;
|
|
435
|
-
assistantText: string;
|
|
436
|
-
nowText: string;
|
|
437
|
-
config: AssistantConfig;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
/**
|
|
441
|
-
* Per-skill breakdown of the raw similarity inputs, captured before any
|
|
442
|
-
* coefficient weighting. Skills have no decay term, so the breakdown is just
|
|
443
|
-
* the three raw sims. Surfaced for telemetry / inspector views.
|
|
444
|
-
*/
|
|
445
|
-
interface SkillActivationBreakdown {
|
|
446
|
-
/** Raw `sim(user, skill)` similarity, before `c_user` weighting. */
|
|
447
|
-
simUser: number;
|
|
448
|
-
/** Raw `sim(assistant, skill)` similarity, before `c_assistant` weighting. */
|
|
449
|
-
simAssistant: number;
|
|
450
|
-
/** Raw `sim(now, skill)` similarity, before `c_now` weighting. */
|
|
451
|
-
simNow: number;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
interface ComputeSkillActivationResult {
|
|
455
|
-
/** Final clamped skill-activation value per id. */
|
|
456
|
-
activation: Map<string, number>;
|
|
457
|
-
/** Per-skill breakdown of the raw sim inputs that fed into `activation`. */
|
|
458
|
-
breakdown: Map<string, SkillActivationBreakdown>;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
/**
|
|
462
|
-
* Apply the skill-side activation formula (no decay carry-over, no spread):
|
|
463
|
-
* A_skill(s) = clamp01(c_user · sim_u + c_assistant · sim_a + c_now · sim_n)
|
|
464
|
-
*
|
|
465
|
-
* Reuses the activation coefficients from `config.memory.v2`. The three
|
|
466
|
-
* `simSkillBatch` calls run concurrently — they hit independent named
|
|
467
|
-
* vectors and embed independent query texts. Returns a per-skill breakdown
|
|
468
|
-
* of the raw sims alongside the activation map so callers can render
|
|
469
|
-
* contribution diagnostics without re-running the math.
|
|
470
|
-
*
|
|
471
|
-
* Empty candidates short-circuits to an empty map without touching the
|
|
472
|
-
* embedding backend or Qdrant.
|
|
473
|
-
*/
|
|
474
|
-
export async function computeSkillActivation(
|
|
475
|
-
params: ComputeSkillActivationParams,
|
|
476
|
-
): Promise<ComputeSkillActivationResult> {
|
|
477
|
-
const { candidates, userText, assistantText, nowText, config } = params;
|
|
478
|
-
|
|
479
|
-
const activation = new Map<string, number>();
|
|
480
|
-
const breakdown = new Map<string, SkillActivationBreakdown>();
|
|
481
|
-
if (candidates.size === 0) return { activation, breakdown };
|
|
482
|
-
|
|
483
|
-
const { c_user, c_assistant, c_now } = config.memory.v2;
|
|
484
|
-
const idList = [...candidates];
|
|
485
|
-
|
|
486
|
-
const [simUser, simAssistant, simNow] = await Promise.all([
|
|
487
|
-
simSkillBatch(userText, idList, config),
|
|
488
|
-
simSkillBatch(assistantText, idList, config),
|
|
489
|
-
simSkillBatch(nowText, idList, config),
|
|
490
|
-
]);
|
|
491
|
-
|
|
492
|
-
for (const id of idList) {
|
|
493
|
-
const simU = simUser.get(id) ?? 0;
|
|
494
|
-
const simA = simAssistant.get(id) ?? 0;
|
|
495
|
-
const simN = simNow.get(id) ?? 0;
|
|
496
|
-
const value = c_user * simU + c_assistant * simA + c_now * simN;
|
|
497
|
-
activation.set(id, clampUnitInterval(value));
|
|
498
|
-
breakdown.set(id, { simUser: simU, simAssistant: simA, simNow: simN });
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
return { activation, breakdown };
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
interface SelectSkillInjectionsParams {
|
|
505
|
-
/** Final skill activation map. */
|
|
506
|
-
A: ReadonlyMap<string, number>;
|
|
507
|
-
/** Cap on the per-turn skill slate, e.g. `config.memory.v2.skills_top_k`. */
|
|
508
|
-
topK: number;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
interface SelectSkillInjectionsResult {
|
|
512
|
-
/**
|
|
513
|
-
* Top-K skill ids by activation (descending), tie-broken lexicographically.
|
|
514
|
-
* Skills are re-presented every turn — no `toInject` delta — so the caller
|
|
515
|
-
* uses this list verbatim to render the skill slate.
|
|
516
|
-
*/
|
|
517
|
-
topNow: string[];
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
/**
|
|
521
|
-
* Pick the top-K skill ids by activation (descending; stable on ties via id
|
|
522
|
-
* lexicographic order). Skills are stateless — there is no `everInjected`
|
|
523
|
-
* dedup, so the same id can appear on consecutive turns.
|
|
524
|
-
*
|
|
525
|
-
* Returns `{ topNow: [] }` for an empty activation map or `topK <= 0`.
|
|
526
|
-
*/
|
|
527
|
-
export function selectSkillInjections(
|
|
528
|
-
params: SelectSkillInjectionsParams,
|
|
529
|
-
): SelectSkillInjectionsResult {
|
|
530
|
-
const { A, topK } = params;
|
|
531
|
-
if (A.size === 0 || topK <= 0) {
|
|
532
|
-
return { topNow: [] };
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
const ranked = [...A.entries()].sort(([idA, valA], [idB, valB]) => {
|
|
536
|
-
if (valB !== valA) return valB - valA; // higher activation first
|
|
537
|
-
return idA < idB ? -1 : idA > idB ? 1 : 0; // stable tie-break
|
|
538
|
-
});
|
|
539
|
-
|
|
540
|
-
const topNow = ranked.slice(0, topK).map(([id]) => id);
|
|
541
|
-
return { topNow };
|
|
542
|
-
}
|
|
@@ -59,6 +59,7 @@ import { INTERNAL_GUARDIAN_TRUST_CONTEXT } from "../../daemon/trust-context.js";
|
|
|
59
59
|
import { wakeAgentForOpportunity } from "../../runtime/agent-wake.js";
|
|
60
60
|
import { getLogger } from "../../util/logger.js";
|
|
61
61
|
import { getWorkspaceDir } from "../../util/platform.js";
|
|
62
|
+
import { isProcessAlive } from "../../util/process-liveness.js";
|
|
62
63
|
import { bootstrapConversation } from "../conversation-bootstrap.js";
|
|
63
64
|
import { deleteConversation } from "../conversation-crud.js";
|
|
64
65
|
import {
|
|
@@ -240,14 +241,20 @@ function readBufferContent(bufferPath: string): string {
|
|
|
240
241
|
/**
|
|
241
242
|
* Atomically create the lock file with `wx` (O_CREAT | O_EXCL) flags. Returns
|
|
242
243
|
* `null` on success, or the current holder string (file contents, typically
|
|
243
|
-
* `pid timestamp`) when the file already exists
|
|
244
|
-
* log diagnostics so operators can identify a stuck lock without re-reading.
|
|
244
|
+
* `pid timestamp`) when the file already exists and the holder is still alive.
|
|
245
245
|
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
246
|
+
* Stale-lock takeover: if the file exists but its holder PID is not running,
|
|
247
|
+
* unlink the stale file and retry the create exactly once. This recovers
|
|
248
|
+
* automatically from a crashed daemon that died with the lock held —
|
|
249
|
+
* otherwise every subsequent scheduled consolidation would skip with `locked`
|
|
250
|
+
* indefinitely until an operator manually removed the file.
|
|
251
|
+
*
|
|
252
|
+
* The simple takeover-then-retry is safe here (unlike `snapshot-lock.ts`'s
|
|
253
|
+
* full rename-aside dance) because only the assistant's jobs worker calls
|
|
254
|
+
* this lock, and at most one assistant process runs per workspace at any
|
|
255
|
+
* time. A holder with an unparseable / empty payload is treated as stale —
|
|
256
|
+
* the only writers ever produce a `<pid> <timestamp>` line, so an
|
|
257
|
+
* unparseable file is corruption from a partial write that crashed.
|
|
251
258
|
*/
|
|
252
259
|
function tryAcquireLock(lockPath: string): string | null {
|
|
253
260
|
// The workspace migration seeds `memory/.v2-state/`, but tests and
|
|
@@ -255,9 +262,40 @@ function tryAcquireLock(lockPath: string): string | null {
|
|
|
255
262
|
// is idempotent, so the call is cheap when the dir already exists.
|
|
256
263
|
mkdirSync(dirname(lockPath), { recursive: true });
|
|
257
264
|
|
|
265
|
+
const firstHolder = tryCreate(lockPath);
|
|
266
|
+
if (firstHolder === null) return null;
|
|
267
|
+
if (!isHolderStale(firstHolder)) return firstHolder;
|
|
268
|
+
|
|
269
|
+
log.info(
|
|
270
|
+
{ lockPath, holder: firstHolder },
|
|
271
|
+
"consolidation: taking over stale lock (holder not running)",
|
|
272
|
+
);
|
|
273
|
+
try {
|
|
274
|
+
unlinkSync(lockPath);
|
|
275
|
+
} catch (err) {
|
|
276
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
277
|
+
if (code !== "ENOENT") {
|
|
278
|
+
log.warn(
|
|
279
|
+
{ err, lockPath },
|
|
280
|
+
"consolidation: failed to unlink stale lock; reporting as locked",
|
|
281
|
+
);
|
|
282
|
+
return firstHolder;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// After unlink, the next `wx` create should succeed. If a third party
|
|
286
|
+
// raced in and re-acquired (vanishingly unlikely with one writer per
|
|
287
|
+
// workspace), surface their holder string rather than overwriting.
|
|
288
|
+
return tryCreate(lockPath);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Atomically create the lock file. Returns `null` on success, or the holder
|
|
293
|
+
* string read from the file when it already exists (`"unknown"` if the read
|
|
294
|
+
* itself fails). Rethrows any non-EEXIST errno from `openSync`.
|
|
295
|
+
*/
|
|
296
|
+
function tryCreate(lockPath: string): string | null {
|
|
258
297
|
let fd: number;
|
|
259
298
|
try {
|
|
260
|
-
// `wx` = create-if-not-exists, fail with EEXIST if it does.
|
|
261
299
|
fd = openSync(lockPath, "wx");
|
|
262
300
|
} catch (err) {
|
|
263
301
|
if ((err as NodeJS.ErrnoException).code !== "EEXIST") throw err;
|
|
@@ -267,13 +305,10 @@ function tryAcquireLock(lockPath: string): string | null {
|
|
|
267
305
|
return "unknown";
|
|
268
306
|
}
|
|
269
307
|
}
|
|
270
|
-
|
|
271
|
-
// Best-effort PID + timestamp payload so a stale lock can be diagnosed.
|
|
272
|
-
// The worker only cares that the file exists; the contents are advisory.
|
|
273
308
|
try {
|
|
274
309
|
writeSync(fd, `${process.pid} ${Date.now()}\n`);
|
|
275
310
|
} catch {
|
|
276
|
-
// best-effort
|
|
311
|
+
// best-effort — payload is advisory, the file's existence is the lock
|
|
277
312
|
} finally {
|
|
278
313
|
try {
|
|
279
314
|
closeSync(fd);
|
|
@@ -284,6 +319,21 @@ function tryAcquireLock(lockPath: string): string | null {
|
|
|
284
319
|
return null;
|
|
285
320
|
}
|
|
286
321
|
|
|
322
|
+
/**
|
|
323
|
+
* A holder string is stale when its PID parses to a non-running process.
|
|
324
|
+
* The payload format is `<pid> <timestamp>` (see `tryCreate`'s write), but
|
|
325
|
+
* an unparseable / empty / `"unknown"` payload is also treated as stale:
|
|
326
|
+
* the only writer is `tryCreate` itself, so corruption indicates a partial
|
|
327
|
+
* write from a crashed prior holder rather than a live writer mid-flush.
|
|
328
|
+
*/
|
|
329
|
+
function isHolderStale(holder: string): boolean {
|
|
330
|
+
const match = /^\d+/.exec(holder);
|
|
331
|
+
if (!match) return true;
|
|
332
|
+
const pid = Number.parseInt(match[0], 10);
|
|
333
|
+
if (!Number.isFinite(pid) || pid <= 0) return true;
|
|
334
|
+
return !isProcessAlive(pid);
|
|
335
|
+
}
|
|
336
|
+
|
|
287
337
|
/**
|
|
288
338
|
* Idempotent unlink of the lock file. Called from the `finally` block so a
|
|
289
339
|
* crash in the wake path doesn't leave the lock stranded. ENOENT is swallowed
|