@vellumai/assistant 0.5.15 → 0.5.16
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 +2 -2
- package/docs/architecture/integrations.md +15 -14
- package/knip.json +3 -1
- package/openapi.yaml +11 -43
- package/package.json +1 -1
- package/src/__tests__/assistant-feature-flags-integration.test.ts +3 -375
- package/src/__tests__/ces-rpc-credential-backend.test.ts +4 -1
- package/src/__tests__/checker.test.ts +59 -0
- package/src/__tests__/cli-command-risk-guard.test.ts +98 -10
- package/src/__tests__/cli-memory.test.ts +372 -0
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +12 -2
- package/src/__tests__/config-schema.test.ts +0 -2
- package/src/__tests__/config-watcher-feature-flags.test.ts +211 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +7 -4
- package/src/__tests__/conversation-slash-commands.test.ts +2 -6
- package/src/__tests__/conversation-usage.test.ts +1 -0
- package/src/__tests__/credential-security-e2e.test.ts +4 -1
- package/src/__tests__/docker-signing-key-bootstrap.test.ts +7 -73
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -7
- package/src/__tests__/guardian-routing-invariants.test.ts +151 -0
- package/src/__tests__/heartbeat-service.test.ts +1 -3
- package/src/__tests__/intent-routing.test.ts +6 -18
- package/src/__tests__/log-export-workspace.test.ts +2 -28
- package/src/__tests__/managed-skill-lifecycle.test.ts +7 -37
- package/src/__tests__/managed-store.test.ts +2 -10
- package/src/__tests__/messaging-send-tool.test.ts +6 -6
- package/src/__tests__/migration-cross-version-compatibility.test.ts +1 -29
- package/src/__tests__/migration-export-http.test.ts +3 -34
- package/src/__tests__/migration-import-commit-http.test.ts +1 -29
- package/src/__tests__/migration-import-preflight-http.test.ts +3 -34
- package/src/__tests__/no-domain-routing-in-prompt-guard.test.ts +2 -1
- package/src/__tests__/oauth-apps-routes.test.ts +120 -10
- package/src/__tests__/oauth-connect-orchestrator.test.ts +709 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +2 -1
- package/src/__tests__/oauth-provider-visibility.test.ts +149 -0
- package/src/__tests__/oauth-providers-routes.test.ts +5 -2
- package/src/__tests__/oauth-store.test.ts +0 -5
- package/src/__tests__/outlook-messaging-provider.test.ts +576 -0
- package/src/__tests__/path-policy.test.ts +2 -17
- package/src/__tests__/permission-types.test.ts +0 -1
- package/src/__tests__/platform-callback-registration.test.ts +3 -7
- package/src/__tests__/provider-commit-message-generator.test.ts +0 -1
- package/src/__tests__/provider-error-scenarios.test.ts +0 -2
- package/src/__tests__/qdrant-manager.test.ts +68 -21
- package/src/__tests__/require-fresh-approval.test.ts +0 -1
- package/src/__tests__/sandbox-diagnostics.test.ts +20 -29
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +2 -10
- package/src/__tests__/secret-allowlist.test.ts +20 -35
- package/src/__tests__/shell-credential-ref.test.ts +0 -5
- package/src/__tests__/skill-load-feature-flag.test.ts +2 -43
- package/src/__tests__/skill-load-inline-command.test.ts +3 -65
- package/src/__tests__/skill-load-inline-includes.test.ts +3 -65
- package/src/__tests__/skill-load-tool.test.ts +3 -67
- package/src/__tests__/skill-memory.test.ts +362 -119
- package/src/__tests__/skills.test.ts +22 -49
- package/src/__tests__/slack-channel-config.test.ts +2 -21
- package/src/__tests__/starter-bundle.test.ts +2 -8
- package/src/__tests__/stt-hints.test.ts +7 -2
- package/src/__tests__/system-prompt.test.ts +25 -45
- package/src/__tests__/task-compiler.test.ts +0 -21
- package/src/__tests__/task-management-tools.test.ts +0 -21
- package/src/__tests__/task-memory-cleanup.test.ts +0 -21
- package/src/__tests__/task-runner.test.ts +0 -21
- package/src/__tests__/task-scheduler.test.ts +0 -21
- package/src/__tests__/terminal-tools.test.ts +1 -17
- package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +0 -79
- package/src/__tests__/tool-approval-handler.test.ts +1 -20
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +2 -11
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -25
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
- package/src/__tests__/tool-executor.test.ts +0 -1
- package/src/__tests__/tool-grant-request-escalation.test.ts +1 -20
- package/src/__tests__/tool-preview-lifecycle.test.ts +0 -20
- package/src/__tests__/trust-store.test.ts +9 -41
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -30
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1 -21
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -22
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -22
- package/src/__tests__/trusted-contact-verification.test.ts +0 -22
- package/src/__tests__/turn-boundary-resolution.test.ts +0 -28
- package/src/__tests__/twilio-provider.test.ts +0 -16
- package/src/__tests__/twilio-routes-twiml.test.ts +7 -12
- package/src/__tests__/twilio-routes.test.ts +0 -24
- package/src/__tests__/update-bulletin.test.ts +17 -89
- package/src/__tests__/usage-cache-backfill-migration.test.ts +0 -20
- package/src/__tests__/usage-routes.test.ts +0 -21
- package/src/__tests__/user-reference.test.ts +1 -5
- package/src/__tests__/vbundle-pax-and-symlink.test.ts +4 -34
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +2 -53
- package/src/__tests__/voice-invite-redemption.test.ts +0 -21
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -24
- package/src/__tests__/voice-session-bridge.test.ts +0 -21
- package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +2 -23
- package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +2 -2
- package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +2 -23
- package/src/__tests__/workspace-migration-down-functions.test.ts +0 -6
- package/src/acp/client-handler.ts +1 -2
- package/src/cli/__tests__/notifications.test.ts +0 -22
- package/src/cli/cli-memory.ts +176 -0
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
- package/src/cli/commands/oauth/connect.ts +15 -0
- package/src/cli/commands/oauth/providers.ts +49 -42
- package/src/cli/commands/platform/__tests__/connect.test.ts +2 -48
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +2 -48
- package/src/cli/commands/platform/__tests__/status.test.ts +0 -50
- package/src/config/bundled-skills/computer-use/TOOLS.json +7 -7
- package/src/config/bundled-skills/messaging/SKILL.md +17 -2
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/loader.ts +4 -0
- package/src/config/schemas/security.ts +0 -6
- package/src/config/schemas/services.ts +8 -0
- package/src/context/window-manager.ts +28 -9
- package/src/credential-execution/approval-bridge.ts +0 -1
- package/src/daemon/config-watcher.ts +51 -0
- package/src/daemon/conversation-agent-loop.ts +3 -2
- package/src/daemon/conversation-process.ts +1 -0
- package/src/daemon/conversation-usage.ts +1 -0
- package/src/daemon/handlers/skills.ts +9 -1
- package/src/daemon/lifecycle.ts +13 -4
- package/src/daemon/message-types/conversations.ts +1 -0
- package/src/daemon/providers-setup.ts +2 -0
- package/src/daemon/server.ts +26 -22
- package/src/events/domain-events.ts +1 -2
- package/src/memory/db-init.ts +9 -0
- package/src/memory/job-handlers/batch-extraction.ts +16 -4
- package/src/memory/job-handlers/embedding.test.ts +3 -27
- package/src/memory/job-handlers/journal-carry-forward.test.ts +1 -29
- package/src/memory/llm-usage-store.ts +35 -2
- package/src/memory/migrations/201-oauth-providers-feature-flag.ts +11 -0
- package/src/memory/migrations/202-drop-callback-transport-column.ts +13 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/qdrant-manager.ts +26 -5
- package/src/memory/query-expansion.ts +1 -1
- package/src/memory/retriever.test.ts +22 -20
- package/src/memory/retriever.ts +10 -2
- package/src/memory/schema/oauth.ts +1 -1
- package/src/memory/search/mmr.ts +8 -5
- package/src/memory/slack-thread-store.ts +17 -0
- package/src/messaging/providers/outlook/adapter.ts +193 -0
- package/src/messaging/providers/outlook/client.ts +311 -0
- package/src/messaging/providers/outlook/types.ts +83 -0
- package/src/notifications/adapters/slack.ts +1 -1
- package/src/oauth/__tests__/identity-verifier.test.ts +1 -1
- package/src/oauth/connect-orchestrator.ts +10 -3
- package/src/oauth/oauth-store.ts +10 -11
- package/src/oauth/provider-serializer.ts +3 -0
- package/src/oauth/provider-visibility.ts +16 -0
- package/src/oauth/seed-providers.ts +49 -17
- package/src/permissions/checker.ts +39 -7
- package/src/permissions/types.ts +2 -4
- package/src/prompts/journal-context.ts +9 -11
- package/src/prompts/system-prompt.ts +3 -64
- package/src/prompts/templates/UPDATES.md +6 -0
- package/src/runtime/auth/__tests__/credential-service.test.ts +1 -27
- package/src/runtime/auth/__tests__/token-service.test.ts +1 -25
- package/src/runtime/auth/route-policy.ts +0 -4
- package/src/runtime/guardian-reply-router.ts +6 -2
- package/src/runtime/routes/conversation-query-routes.ts +2 -58
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +43 -2
- package/src/runtime/routes/memory-item-routes.test.ts +0 -17
- package/src/runtime/routes/memory-item-routes.ts +103 -12
- package/src/runtime/routes/oauth-apps.ts +18 -1
- package/src/runtime/routes/oauth-providers.ts +13 -1
- package/src/runtime/routes/settings-routes.ts +1 -0
- package/src/runtime/routes/usage-routes.ts +19 -2
- package/src/runtime/routes/work-items-routes.test.ts +0 -21
- package/src/runtime/routes/workspace-routes.test.ts +3 -27
- package/src/security/secret-allowlist.ts +4 -4
- package/src/skills/skill-memory.ts +62 -23
- package/src/tools/memory/handlers.test.ts +1 -29
- package/src/tools/permission-checker.ts +0 -18
- package/src/tools/skills/skill-script-runner.ts +1 -1
- package/src/util/device-id.ts +3 -65
- package/src/workspace/git-service.ts +27 -6
|
@@ -1,29 +1,58 @@
|
|
|
1
1
|
import { and, eq } from "drizzle-orm";
|
|
2
2
|
import { v4 as uuid } from "uuid";
|
|
3
3
|
|
|
4
|
-
import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
|
|
5
4
|
import { getConfig } from "../config/loader.js";
|
|
5
|
+
import { resolveSkillStates } from "../config/skill-state.js";
|
|
6
|
+
import { loadSkillCatalog, type SkillSummary } from "../config/skills.js";
|
|
6
7
|
import { getDb } from "../memory/db.js";
|
|
7
8
|
import { computeMemoryFingerprint } from "../memory/fingerprint.js";
|
|
8
9
|
import { enqueueMemoryJob } from "../memory/jobs-store.js";
|
|
9
10
|
import { memoryItems } from "../memory/schema.js";
|
|
10
11
|
import { getLogger } from "../util/logger.js";
|
|
11
|
-
import { type CatalogSkill, resolveCatalog } from "./catalog-install.js";
|
|
12
12
|
|
|
13
13
|
const log = getLogger("skill-memory");
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
16
|
+
* Generic input for building capability statements.
|
|
17
|
+
* Decoupled from CatalogSkill so other skill sources (e.g. bundled skills) can
|
|
18
|
+
* produce capability memories without being shoehorned into the catalog type.
|
|
19
|
+
*/
|
|
20
|
+
export interface SkillCapabilityInput {
|
|
21
|
+
id: string;
|
|
22
|
+
displayName: string;
|
|
23
|
+
description: string;
|
|
24
|
+
activationHints?: string[];
|
|
25
|
+
avoidWhen?: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Convert a SkillSummary to a SkillCapabilityInput.
|
|
30
|
+
* SkillSummary already has flat properties, so this is a straightforward mapping.
|
|
31
|
+
*/
|
|
32
|
+
export function fromSkillSummary(entry: SkillSummary): SkillCapabilityInput {
|
|
33
|
+
return {
|
|
34
|
+
id: entry.id,
|
|
35
|
+
displayName: entry.displayName,
|
|
36
|
+
description: entry.description,
|
|
37
|
+
activationHints: entry.activationHints,
|
|
38
|
+
avoidWhen: entry.avoidWhen,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build a semantically rich capability statement from a skill capability input.
|
|
17
44
|
* Truncated to 500 chars max (matching the limit used by memory item extraction).
|
|
18
45
|
*/
|
|
19
|
-
export function buildCapabilityStatement(
|
|
20
|
-
const displayName
|
|
21
|
-
const activationHints = entry.metadata?.vellum?.["activation-hints"];
|
|
46
|
+
export function buildCapabilityStatement(input: SkillCapabilityInput): string {
|
|
47
|
+
const { displayName, activationHints, avoidWhen } = input;
|
|
22
48
|
|
|
23
|
-
let statement = `The "${displayName}" skill (${
|
|
49
|
+
let statement = `The "${displayName}" skill (${input.id}) is available. ${input.description}.`;
|
|
24
50
|
if (activationHints && activationHints.length > 0) {
|
|
25
51
|
statement += ` Use when: ${activationHints.join("; ")}.`;
|
|
26
52
|
}
|
|
53
|
+
if (avoidWhen && avoidWhen.length > 0) {
|
|
54
|
+
statement += ` Avoid when: ${avoidWhen.join("; ")}.`;
|
|
55
|
+
}
|
|
27
56
|
|
|
28
57
|
// Truncate to 500 chars max
|
|
29
58
|
if (statement.length > 500) {
|
|
@@ -34,17 +63,17 @@ export function buildCapabilityStatement(entry: CatalogSkill): string {
|
|
|
34
63
|
}
|
|
35
64
|
|
|
36
65
|
/**
|
|
37
|
-
* Upsert a capability memory item for a
|
|
66
|
+
* Upsert a capability memory item for a skill.
|
|
38
67
|
* Best-effort: errors are logged but never thrown.
|
|
39
68
|
*/
|
|
40
69
|
export function upsertSkillCapabilityMemory(
|
|
41
70
|
skillId: string,
|
|
42
|
-
|
|
71
|
+
input: SkillCapabilityInput,
|
|
43
72
|
): void {
|
|
44
73
|
try {
|
|
45
74
|
const db = getDb();
|
|
46
75
|
const subject = `skill:${skillId}`;
|
|
47
|
-
const statement = buildCapabilityStatement(
|
|
76
|
+
const statement = buildCapabilityStatement(input);
|
|
48
77
|
const kind = "capability";
|
|
49
78
|
const scopeId = "default";
|
|
50
79
|
const confidence = 1.0;
|
|
@@ -169,30 +198,39 @@ export function deleteSkillCapabilityMemory(skillId: string): void {
|
|
|
169
198
|
}
|
|
170
199
|
|
|
171
200
|
/**
|
|
172
|
-
* Seed capability memory items for all
|
|
173
|
-
* Prunes stale entries whose skills are no longer in the
|
|
201
|
+
* Seed capability memory items for all enabled skills (bundled, managed, workspace, extra).
|
|
202
|
+
* Prunes stale entries whose skills are no longer in the enabled set.
|
|
174
203
|
* Best-effort: errors are logged but never thrown.
|
|
175
204
|
*/
|
|
176
|
-
export
|
|
205
|
+
export function seedCatalogSkillMemories(): void {
|
|
177
206
|
try {
|
|
178
|
-
const catalog =
|
|
207
|
+
const catalog = loadSkillCatalog();
|
|
179
208
|
const config = getConfig();
|
|
209
|
+
const resolved = resolveSkillStates(catalog, config);
|
|
210
|
+
const enabled = resolved.filter((r) => r.state === "enabled");
|
|
211
|
+
|
|
180
212
|
const catalogIds = new Set<string>();
|
|
213
|
+
for (const { summary } of enabled) {
|
|
214
|
+
catalogIds.add(summary.id);
|
|
215
|
+
const input = fromSkillSummary(summary);
|
|
181
216
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
217
|
+
// Enrich mcp-setup description with configured server names
|
|
218
|
+
if (summary.id === "mcp-setup") {
|
|
219
|
+
const servers = config.mcp?.servers;
|
|
220
|
+
if (servers) {
|
|
221
|
+
const names = Object.keys(servers).filter(
|
|
222
|
+
(name) => servers[name]?.enabled !== false,
|
|
223
|
+
);
|
|
224
|
+
if (names.length > 0) {
|
|
225
|
+
input.description += ` Configured: ${names.join(", ")}`;
|
|
226
|
+
}
|
|
188
227
|
}
|
|
189
228
|
}
|
|
190
229
|
|
|
191
|
-
|
|
192
|
-
upsertSkillCapabilityMemory(entry.id, entry);
|
|
230
|
+
upsertSkillCapabilityMemory(summary.id, input);
|
|
193
231
|
}
|
|
194
232
|
|
|
195
|
-
// Prune stale capability memories for skills no longer in
|
|
233
|
+
// Prune stale capability memories for skills no longer in the enabled set
|
|
196
234
|
const db = getDb();
|
|
197
235
|
const allCapabilities = db
|
|
198
236
|
.select()
|
|
@@ -208,6 +246,7 @@ export async function seedCatalogSkillMemories(): Promise<void> {
|
|
|
208
246
|
|
|
209
247
|
const now = Date.now();
|
|
210
248
|
for (const item of allCapabilities) {
|
|
249
|
+
if (!item.subject.startsWith("skill:")) continue;
|
|
211
250
|
const itemSkillId = item.subject.replace("skill:", "");
|
|
212
251
|
if (!catalogIds.has(itemSkillId)) {
|
|
213
252
|
db.update(memoryItems)
|
|
@@ -4,34 +4,10 @@
|
|
|
4
4
|
* Covers happy path (multi-source results), empty results, degraded mode
|
|
5
5
|
* (embeddings/Qdrant unavailable), scope filtering, and error handling.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
8
|
-
import { tmpdir } from "node:os";
|
|
9
|
-
import { join } from "node:path";
|
|
10
|
-
import {
|
|
11
|
-
afterAll,
|
|
12
|
-
beforeAll,
|
|
13
|
-
beforeEach,
|
|
14
|
-
describe,
|
|
15
|
-
expect,
|
|
16
|
-
mock,
|
|
17
|
-
test,
|
|
18
|
-
} from "bun:test";
|
|
19
|
-
|
|
20
|
-
const testDir = mkdtempSync(join(tmpdir(), "memory-recall-handler-"));
|
|
7
|
+
import { beforeAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
21
8
|
|
|
22
9
|
// ── Module mocks (must precede production imports) ───────────────────
|
|
23
10
|
|
|
24
|
-
mock.module("../../util/platform.js", () => ({
|
|
25
|
-
getDataDir: () => testDir,
|
|
26
|
-
isMacOS: () => process.platform === "darwin",
|
|
27
|
-
isLinux: () => process.platform === "linux",
|
|
28
|
-
isWindows: () => process.platform === "win32",
|
|
29
|
-
getPidPath: () => join(testDir, "test.pid"),
|
|
30
|
-
getDbPath: () => join(testDir, "test.db"),
|
|
31
|
-
getLogPath: () => join(testDir, "test.log"),
|
|
32
|
-
ensureDataDir: () => {},
|
|
33
|
-
}));
|
|
34
|
-
|
|
35
11
|
mock.module("../../util/logger.js", () => ({
|
|
36
12
|
getLogger: () =>
|
|
37
13
|
new Proxy({} as Record<string, unknown>, {
|
|
@@ -259,10 +235,6 @@ describe("handleMemoryRecall", () => {
|
|
|
259
235
|
clearEmbeddingBackendCache();
|
|
260
236
|
});
|
|
261
237
|
|
|
262
|
-
afterAll(() => {
|
|
263
|
-
rmSync(testDir, { recursive: true, force: true });
|
|
264
|
-
});
|
|
265
|
-
|
|
266
238
|
// ── Input validation ──────────────────────────────────────────────
|
|
267
239
|
|
|
268
240
|
test("returns error when query is missing", async () => {
|
|
@@ -137,24 +137,6 @@ export class PermissionChecker {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
if (result.decision === "prompt") {
|
|
140
|
-
// dangerouslySkipPermissions: when enabled, auto-approve all prompts
|
|
141
|
-
// without user interaction. Deny rules are still respected (they
|
|
142
|
-
// return before reaching this block).
|
|
143
|
-
//
|
|
144
|
-
// Note: unlike guardian auto-approve and temporary overrides below,
|
|
145
|
-
// this intentionally does NOT check `context.requireFreshApproval`.
|
|
146
|
-
// The setting is designed to skip ALL interactive prompts
|
|
147
|
-
// unconditionally — it is an explicit operator opt-out from the
|
|
148
|
-
// permission system, so requireFreshApproval does not apply.
|
|
149
|
-
const cfg = getConfig();
|
|
150
|
-
if (cfg.permissions.dangerouslySkipPermissions) {
|
|
151
|
-
log.info(
|
|
152
|
-
{ toolName: name, riskLevel },
|
|
153
|
-
"dangerouslySkipPermissions active — auto-approving without prompt",
|
|
154
|
-
);
|
|
155
|
-
return { allowed: true, decision: "dangerously_skip_permissions", riskLevel };
|
|
156
|
-
}
|
|
157
|
-
|
|
158
140
|
// Guardian-trust sessions (e.g. scheduled jobs, reminders) should be
|
|
159
141
|
// able to use bundled tools without interactive approval. The guardian
|
|
160
142
|
// is the owner - prompting makes no sense when there is no client.
|
|
@@ -40,7 +40,7 @@ export async function runSkillToolScript(
|
|
|
40
40
|
context: ToolContext,
|
|
41
41
|
options?: RunSkillToolScriptOptions,
|
|
42
42
|
): Promise<ToolExecutionResult> {
|
|
43
|
-
if (options?.target === "sandbox") {
|
|
43
|
+
if (options?.target === "sandbox" && !options?.bundled) {
|
|
44
44
|
return runSkillToolScriptSandbox(skillDir, executorPath, input, context, {
|
|
45
45
|
timeoutMs: options.timeoutMs,
|
|
46
46
|
expectedSkillVersionHash: options.expectedSkillVersionHash,
|
package/src/util/device-id.ts
CHANGED
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
* Path resolution:
|
|
9
9
|
* - Containerized (IS_CONTAINERIZED=true): uses /home/assistant (the assistant
|
|
10
10
|
* user's persistent home dir) so device.json lives on the assistant's own
|
|
11
|
-
* filesystem rather than the shared data volume.
|
|
12
|
-
* BASE_DATA_DIR location for migration.
|
|
11
|
+
* filesystem rather than the shared data volume.
|
|
13
12
|
* - Local (single or multi-instance): uses homedir() so all instances on the
|
|
14
13
|
* same machine share a single device ID.
|
|
15
14
|
*
|
|
@@ -45,22 +44,6 @@ export function getDeviceIdBaseDir(): string {
|
|
|
45
44
|
return homedir();
|
|
46
45
|
}
|
|
47
46
|
|
|
48
|
-
/**
|
|
49
|
-
* Resolve the legacy base directory for device.json migration.
|
|
50
|
-
*
|
|
51
|
-
* Returns the old containerized path (via BASE_DATA_DIR env var) so we can
|
|
52
|
-
* fall back to reading device.json from the shared volume if it hasn't been
|
|
53
|
-
* migrated yet. Returns undefined when not containerized or when no legacy
|
|
54
|
-
* path exists.
|
|
55
|
-
*/
|
|
56
|
-
function getLegacyDeviceIdBaseDir(): string | undefined {
|
|
57
|
-
if (!getIsContainerized()) {
|
|
58
|
-
return undefined;
|
|
59
|
-
}
|
|
60
|
-
const baseDataDir = process.env.BASE_DATA_DIR?.trim() || undefined;
|
|
61
|
-
return baseDataDir;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
47
|
/**
|
|
65
48
|
* Get the stable device ID for this machine.
|
|
66
49
|
*
|
|
@@ -96,55 +79,10 @@ export function getDeviceId(): string {
|
|
|
96
79
|
}
|
|
97
80
|
}
|
|
98
81
|
} catch (err) {
|
|
99
|
-
log.warn({ err }, "Failed to read device.json —
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Migration fallback: check the legacy location (shared volume) if the new
|
|
103
|
-
// location doesn't have a valid device.json yet.
|
|
104
|
-
const legacyBase = getLegacyDeviceIdBaseDir();
|
|
105
|
-
if (legacyBase) {
|
|
106
|
-
const legacyPath = join(legacyBase, ".vellum", "device.json");
|
|
107
|
-
try {
|
|
108
|
-
if (existsSync(legacyPath)) {
|
|
109
|
-
const raw = JSON.parse(readFileSync(legacyPath, "utf-8"));
|
|
110
|
-
if (
|
|
111
|
-
raw &&
|
|
112
|
-
typeof raw === "object" &&
|
|
113
|
-
typeof raw.deviceId === "string" &&
|
|
114
|
-
raw.deviceId.length > 0
|
|
115
|
-
) {
|
|
116
|
-
cached = raw.deviceId as string;
|
|
117
|
-
log.info(
|
|
118
|
-
{ deviceId: cached },
|
|
119
|
-
"Resolved device ID from legacy device.json — will persist to new location",
|
|
120
|
-
);
|
|
121
|
-
// Persist to the new location so future reads don't need the fallback
|
|
122
|
-
try {
|
|
123
|
-
mkdirSync(vellumDir, { recursive: true });
|
|
124
|
-
writeFileSync(
|
|
125
|
-
filePath,
|
|
126
|
-
JSON.stringify({ deviceId: cached }, null, 2) + "\n",
|
|
127
|
-
{ mode: 0o644 },
|
|
128
|
-
);
|
|
129
|
-
log.info("Migrated device.json to new location");
|
|
130
|
-
} catch (writeErr) {
|
|
131
|
-
log.warn(
|
|
132
|
-
{ err: writeErr },
|
|
133
|
-
"Failed to migrate device.json to new location",
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
return cached;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
} catch (err) {
|
|
140
|
-
log.warn(
|
|
141
|
-
{ err },
|
|
142
|
-
"Failed to read legacy device.json — generating new device ID",
|
|
143
|
-
);
|
|
144
|
-
}
|
|
82
|
+
log.warn({ err }, "Failed to read device.json — generating new device ID");
|
|
145
83
|
}
|
|
146
84
|
|
|
147
|
-
// Either the file doesn't exist
|
|
85
|
+
// Either the file doesn't exist or deviceId was missing/empty.
|
|
148
86
|
// Generate a new UUID and persist it.
|
|
149
87
|
try {
|
|
150
88
|
mkdirSync(vellumDir, { recursive: true });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
1
|
+
import { execFile, spawnSync } from "node:child_process";
|
|
2
2
|
import {
|
|
3
3
|
existsSync,
|
|
4
4
|
readFileSync,
|
|
@@ -258,20 +258,41 @@ export class WorkspaceGitService {
|
|
|
258
258
|
}
|
|
259
259
|
|
|
260
260
|
/**
|
|
261
|
-
* Remove `.git/index.lock` if it exists.
|
|
261
|
+
* Remove `.git/index.lock` if it exists and no external process holds it.
|
|
262
262
|
*
|
|
263
263
|
* This method is always called inside the mutex, so no git operation from
|
|
264
|
-
* our code can be concurrently holding the lock.
|
|
265
|
-
*
|
|
266
|
-
* has
|
|
264
|
+
* our code can be concurrently holding the lock. However, an external git
|
|
265
|
+
* process (user running `git add`, IDE tooling, etc.) could legitimately
|
|
266
|
+
* hold the lock. We use `lsof` to check — if any process has the file
|
|
267
|
+
* open, we leave it alone. If no process holds it, it's stale (crashed
|
|
268
|
+
* process) and safe to remove.
|
|
267
269
|
*/
|
|
268
270
|
private cleanStaleLockFile(): void {
|
|
269
271
|
const lockPath = join(this.workspaceDir, ".git", "index.lock");
|
|
272
|
+
if (!existsSync(lockPath)) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
const result = spawnSync("lsof", ["-t", lockPath], {
|
|
278
|
+
timeout: 3000,
|
|
279
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
280
|
+
});
|
|
281
|
+
if (result.status === 0 && result.stdout?.length > 0) {
|
|
282
|
+
log.debug("index.lock held by an active process, skipping removal");
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
} catch {
|
|
286
|
+
// lsof unavailable or errored — fall through to remove.
|
|
287
|
+
// On platforms without lsof this degrades to unconditional removal,
|
|
288
|
+
// which is the same as the previous behavior.
|
|
289
|
+
}
|
|
290
|
+
|
|
270
291
|
try {
|
|
271
292
|
unlinkSync(lockPath);
|
|
272
293
|
log.debug("Removed stale index.lock");
|
|
273
294
|
} catch {
|
|
274
|
-
// File
|
|
295
|
+
// File was removed between check and unlink, or can't be removed — move on.
|
|
275
296
|
}
|
|
276
297
|
}
|
|
277
298
|
|