@vellumai/assistant 0.11.2-dev.202608060701.ede4331 → 0.11.2-dev.202608062308.5442cf1
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/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +6 -0
- package/node_modules/@vellumai/environments/package.json +2 -1
- package/node_modules/@vellumai/environments/src/__tests__/cloud-assistant-hub-url.test.ts +38 -0
- package/node_modules/@vellumai/environments/src/seeds.ts +29 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +6 -0
- package/node_modules/@vellumai/ipc-server-utils/src/endpoint.test.ts +36 -0
- package/node_modules/@vellumai/ipc-server-utils/src/endpoint.ts +142 -0
- package/node_modules/@vellumai/ipc-server-utils/src/index.ts +2 -0
- package/node_modules/@vellumai/ipc-server-utils/src/listen-options.ts +3 -0
- package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +15 -1
- package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +17 -2
- package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +6 -0
- package/openapi.yaml +81 -4
- package/package.json +1 -1
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +1 -0
- package/src/__tests__/agent-wake-override-profile.test.ts +1 -0
- package/src/__tests__/canned-reply-release.test.ts +86 -0
- package/src/__tests__/channel-readiness-telegram-remote.test.ts +154 -0
- package/src/__tests__/client-os-metadata-persistence.test.ts +4 -4
- package/src/__tests__/conversation-agent-loop.test.ts +130 -1
- package/src/__tests__/conversation-fork-referential.test.ts +352 -0
- package/src/__tests__/conversation-key-store-origin.test.ts +105 -0
- package/src/__tests__/conversation-origin-at-creation.test.ts +153 -0
- package/src/__tests__/conversation-queue.test.ts +80 -0
- package/src/__tests__/conversation-retry-route.test.ts +2 -0
- package/src/__tests__/conversation-store-ephemeral.test.ts +4 -0
- package/src/__tests__/conversation-wait-for-idle.test.ts +6 -11
- package/src/__tests__/credential-prompt-route.test.ts +57 -7
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/document-sync-tags.test.ts +379 -0
- package/src/__tests__/document-update-default-surface.test.ts +28 -0
- package/src/__tests__/ensure-conversation-exists.test.ts +8 -5
- package/src/__tests__/evict-conversations-for-reload.test.ts +90 -0
- package/src/__tests__/list-messages-queued.test.ts +43 -0
- package/src/__tests__/plugin-api-webhook-url.test.ts +170 -0
- package/src/__tests__/processing-flag-persist-failure.test.ts +15 -10
- package/src/__tests__/queued-message-cancel-and-steer.test.ts +189 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +1 -1
- package/src/__tests__/tool-side-effects-documents.test.ts +180 -0
- package/src/__tests__/turn-tail-chain.test.ts +135 -0
- package/src/api/constants/document-tools.ts +55 -0
- package/src/api/index.ts +7 -0
- package/src/channels/config.ts +7 -2
- package/src/channels/types.ts +7 -1
- package/src/cli/commands/credentials.help.ts +7 -0
- package/src/config/__tests__/memory-retrospective-schema.test.ts +40 -0
- package/src/config/__tests__/plugin-updates-schema.test.ts +43 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +1 -1
- package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +3 -3
- package/src/config/feature-flag-registry.json +1 -1
- package/src/config/sanitize-for-transfer.ts +11 -0
- package/src/config/schema.ts +4 -0
- package/src/config/schemas/channels.ts +13 -0
- package/src/config/schemas/memory-retrospective.ts +25 -0
- package/src/config/schemas/plugin-updates.ts +55 -0
- package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +2 -4
- package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +20 -29
- package/src/daemon/conversation-agent-loop-handlers.ts +3 -4
- package/src/daemon/conversation-agent-loop.ts +214 -144
- package/src/daemon/conversation-lifecycle.ts +40 -20
- package/src/daemon/conversation-messaging.ts +17 -16
- package/src/daemon/conversation-process.ts +36 -9
- package/src/daemon/conversation-runtime-assembly.ts +2 -2
- package/src/daemon/conversation-store.ts +52 -3
- package/src/daemon/conversation-turn-finalize.ts +134 -89
- package/src/daemon/conversation.ts +66 -22
- package/src/daemon/external-plugins-bootstrap.ts +7 -1
- package/src/daemon/handlers/conversations.ts +24 -6
- package/src/daemon/handlers/skills.ts +10 -0
- package/src/daemon/message-types/conversations.ts +6 -7
- package/src/daemon/message-types/sync.ts +1 -0
- package/src/daemon/tool-side-effects.ts +14 -1
- package/src/daemon/turn-tail-chain.ts +73 -0
- package/src/ipc/__tests__/socket-path.test.ts +21 -0
- package/src/ipc/assistant-server.ts +17 -10
- package/src/ipc/cli-client.ts +10 -1
- package/src/ipc/routes/__tests__/documents-sync-ipc-routes.test.ts +56 -0
- package/src/ipc/routes/documents-sync-ipc-routes.ts +42 -0
- package/src/ipc/socket-cleanup.ts +9 -7
- package/src/ipc/socket-path.ts +4 -96
- package/src/live-voice/__tests__/live-voice-attach-image.test.ts +209 -0
- package/src/live-voice/__tests__/live-voice-connection.test.ts +20 -0
- package/src/live-voice/live-voice-connection.ts +7 -1
- package/src/live-voice/live-voice-photo.ts +169 -0
- package/src/live-voice/live-voice-session.ts +48 -3
- package/src/live-voice/protocol.ts +66 -1
- package/src/messaging/providers/__tests__/callback-routing.test.ts +5 -1
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +66 -4
- package/src/messaging/providers/callback-routing.ts +1 -0
- package/src/messaging/providers/discord/api.ts +222 -0
- package/src/messaging/providers/discord/render.test.ts +204 -0
- package/src/messaging/providers/discord/render.ts +195 -0
- package/src/messaging/providers/discord/send.test.ts +179 -0
- package/src/messaging/providers/discord/send.ts +212 -0
- package/src/messaging/providers/discord/transport.ts +68 -0
- package/src/messaging/providers/index.ts +3 -1
- package/src/messaging/providers/retry-policy.test.ts +247 -0
- package/src/messaging/providers/retry-policy.ts +206 -0
- package/src/messaging/providers/telegram-bot/api.ts +47 -130
- package/src/messaging/providers/whatsapp/api.ts +47 -136
- package/src/monitoring/__tests__/plugin-auto-update.test.ts +332 -0
- package/src/monitoring/plugin-auto-update.ts +359 -0
- package/src/monitoring/worker.ts +13 -0
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +1 -0
- package/src/persistence/__tests__/conversation-lineage.test.ts +248 -0
- package/src/persistence/conversation-crud.ts +222 -66
- package/src/persistence/conversation-key-store.ts +15 -1
- package/src/persistence/conversation-lineage.ts +226 -0
- package/src/persistence/conversation-types.ts +49 -1
- package/src/persistence/migrations/365-add-conversation-fork-strategy.ts +27 -0
- package/src/persistence/schema/conversations.ts +10 -0
- package/src/persistence/steps.ts +2 -0
- package/src/plugin-api/index.ts +7 -0
- package/src/plugin-api/webhook-url.ts +151 -0
- package/src/plugins/defaults/memory/__tests__/jobs-worker-retrospective-sweep.test.ts +24 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +48 -3
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +17 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +1 -0
- package/src/plugins/defaults/memory/jobs-worker.ts +10 -0
- package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +29 -0
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +14 -0
- package/src/plugins/types.ts +3 -3
- package/src/runtime/AGENTS.md +1 -1
- package/src/runtime/__tests__/agent-wake.test.ts +16 -16
- package/src/runtime/agent-wake.ts +58 -24
- package/src/runtime/channel-readiness-service.ts +59 -3
- package/src/runtime/channel-readiness-types.ts +18 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +84 -0
- package/src/runtime/routes/__tests__/retrospective-routes.test.ts +20 -0
- package/src/runtime/routes/__tests__/skill-history-route.test.ts +116 -0
- package/src/runtime/routes/canned-reply-release.ts +54 -0
- package/src/runtime/routes/conversation-list-routes.ts +7 -0
- package/src/runtime/routes/conversation-routes.ts +151 -110
- package/src/runtime/routes/credential-prompt-routes.ts +23 -2
- package/src/runtime/routes/documents-routes.ts +44 -1
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +14 -0
- package/src/runtime/routes/plugins-routes.ts +50 -0
- package/src/runtime/routes/retrospective-routes.ts +9 -3
- package/src/runtime/routes/skills-routes.ts +79 -0
- package/src/runtime/services/__tests__/conversation-serializer.test.ts +1 -0
- package/src/runtime/services/conversation-serializer.ts +24 -9
- package/src/runtime/sync/documents-sidecar-publish.test.ts +101 -0
- package/src/runtime/sync/resource-sync-events.ts +79 -1
- package/src/runtime/sync/worker-daemon-notify.test.ts +34 -0
- package/src/runtime/sync/worker-daemon-notify.ts +44 -6
- package/src/skills/skill-history.test.ts +269 -0
- package/src/skills/skill-history.ts +250 -0
- package/src/telegram/__tests__/webhook-health.test.ts +73 -1
- package/src/telegram/webhook-health.ts +101 -3
- package/src/telemetry/types.ts +3 -4
- package/src/tools/document/document-tool.ts +11 -4
- package/src/workspace/git-service.ts +23 -0
|
@@ -21,6 +21,12 @@
|
|
|
21
21
|
* `Date#toISOString()`.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Pairing-challenge TTL in milliseconds (10 minutes): the gateway's challenge
|
|
26
|
+
* store enforces it and the `vellum pair` CLI renders it in user-facing copy.
|
|
27
|
+
*/
|
|
28
|
+
export const REMOTE_WEB_PAIRING_CODE_TTL_MS = 10 * 60 * 1000;
|
|
29
|
+
|
|
24
30
|
/** `POST /v1/remote-web/pairing-challenge` request body. */
|
|
25
31
|
export interface RemoteWebPairingChallengeRequest {
|
|
26
32
|
/** Public https base URL the scanning device can reach the assistant at. */
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the environment-to-cloud-hub mapping in one place. Both consumers
|
|
3
|
+
* (the Capacitor shell's `server.url` and the CLI remote-web edge's
|
|
4
|
+
* `hubUrl`) read this helper, so this matrix is the single behavioral pin
|
|
5
|
+
* for the served URLs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from "bun:test";
|
|
9
|
+
|
|
10
|
+
import { cloudAssistantHubUrl, SEEDS } from "../index.js";
|
|
11
|
+
|
|
12
|
+
describe("cloudAssistantHubUrl", () => {
|
|
13
|
+
test("cloud environments resolve to their own web origin", () => {
|
|
14
|
+
expect(cloudAssistantHubUrl("production")).toBe(
|
|
15
|
+
"https://www.vellum.ai/assistant",
|
|
16
|
+
);
|
|
17
|
+
expect(cloudAssistantHubUrl("staging")).toBe(
|
|
18
|
+
"https://staging-assistant.vellum.ai/assistant",
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("every other environment falls back to the dev SPA", () => {
|
|
23
|
+
for (const name of ["dev", "test", "local", "unknown-env", undefined]) {
|
|
24
|
+
expect(cloudAssistantHubUrl(name)).toBe(
|
|
25
|
+
"https://dev-assistant.vellum.ai/assistant",
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("cloud hub URLs derive from the seed web URLs", () => {
|
|
31
|
+
for (const name of ["production", "staging"]) {
|
|
32
|
+
expect(cloudAssistantHubUrl(name)).toBe(
|
|
33
|
+
`${SEEDS[name].webUrl}/assistant`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
expect(cloudAssistantHubUrl("local")).toBe(`${SEEDS.dev.webUrl}/assistant`);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -24,6 +24,35 @@ function portBlock(base: number): PortMap {
|
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Base URL of the cloud-hosted assistant SPA (the "hub") for a deployment
|
|
29
|
+
* environment. The Capacitor mobile shell bakes this into native builds as
|
|
30
|
+
* `server.url`, and the CLI's remote-web edge stamps it into the served
|
|
31
|
+
* config as `hubUrl`, so the environment-to-hub mapping lives here exactly
|
|
32
|
+
* once.
|
|
33
|
+
*
|
|
34
|
+
* Only the cloud deployments (production, staging) serve the hosted SPA at
|
|
35
|
+
* their own web origin; every other environment (dev, test, local, unknown)
|
|
36
|
+
* falls back to the dev SPA, whose host serves remote clients that cannot
|
|
37
|
+
* reach a local or per-developer web URL.
|
|
38
|
+
*
|
|
39
|
+
* The `/assistant` suffix is deliberate: booting on the bare host lands on
|
|
40
|
+
* the marketing page, whose CTA redirects to `www.vellum.ai/assistant` and
|
|
41
|
+
* bounces non-prod shells off their own host.
|
|
42
|
+
*
|
|
43
|
+
* NOTE: this module is loaded by Capacitor's single-file TS config loader
|
|
44
|
+
* (via the `@vellumai/environments/seeds` subpath), which cannot follow the
|
|
45
|
+
* package index's `.js`-suffixed runtime re-exports. Keep this file's
|
|
46
|
+
* imports type-only so it stays loadable on its own.
|
|
47
|
+
*/
|
|
48
|
+
export function cloudAssistantHubUrl(envName: string | undefined): string {
|
|
49
|
+
const seed =
|
|
50
|
+
envName === "production" || envName === "staging"
|
|
51
|
+
? SEEDS[envName]
|
|
52
|
+
: SEEDS.dev;
|
|
53
|
+
return `${seed.webUrl}/assistant`;
|
|
54
|
+
}
|
|
55
|
+
|
|
27
56
|
/**
|
|
28
57
|
* Built-in environment definitions and the source of truth for the
|
|
29
58
|
* set of known environment names.
|
|
@@ -21,6 +21,12 @@
|
|
|
21
21
|
* `Date#toISOString()`.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Pairing-challenge TTL in milliseconds (10 minutes): the gateway's challenge
|
|
26
|
+
* store enforces it and the `vellum pair` CLI renders it in user-facing copy.
|
|
27
|
+
*/
|
|
28
|
+
export const REMOTE_WEB_PAIRING_CODE_TTL_MS = 10 * 60 * 1000;
|
|
29
|
+
|
|
24
30
|
/** `POST /v1/remote-web/pairing-challenge` request body. */
|
|
25
31
|
export interface RemoteWebPairingChallengeRequest {
|
|
26
32
|
/** Public https base URL the scanning device can reach the assistant at. */
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { removeIpcEndpointFile, resolveIpcEndpoint } from "./endpoint.js";
|
|
6
|
+
const windowsEndpoint = (name: string, workspaceDir: string) => resolveIpcEndpoint(name, { workspaceDir, platform: "win32" });
|
|
7
|
+
describe("resolveIpcEndpoint", () => {
|
|
8
|
+
test("keeps long POSIX fallbacks deterministic and bounded", () => {
|
|
9
|
+
const options = {
|
|
10
|
+
workspaceDir: `/tmp/${"a".repeat(120)}`,
|
|
11
|
+
platform: "darwin" as const,
|
|
12
|
+
};
|
|
13
|
+
const endpoint = resolveIpcEndpoint("assistant", options);
|
|
14
|
+
expect(resolveIpcEndpoint("assistant", options)).toEqual(endpoint);
|
|
15
|
+
expect(Buffer.byteLength(endpoint.path)).toBeLessThanOrEqual(103);
|
|
16
|
+
});
|
|
17
|
+
test("returns normalized, isolated, bounded Windows pipes", () => {
|
|
18
|
+
const first = windowsEndpoint("assistant", "C:\\one");
|
|
19
|
+
expect(resolveIpcEndpoint("assistant", { workspaceDir: "c:\\ONE\\", platform: "win32", env: { ASSISTANT_IPC_SOCKET_DIR: "C:\\ignored" } })).toEqual(first);
|
|
20
|
+
expect(first.path.length).toBeLessThanOrEqual(256);
|
|
21
|
+
expect(windowsEndpoint("assistant", "C:\\two").path).not.toBe(first.path);
|
|
22
|
+
expect(windowsEndpoint("gateway", "C:\\one").path).not.toBe(first.path);
|
|
23
|
+
});
|
|
24
|
+
test("cleans socket files but skips named pipes", () => {
|
|
25
|
+
const dir = mkdtempSync(join(tmpdir(), "vellum-ipc-cleanup-"));
|
|
26
|
+
const socketPath = join(dir, "assistant.sock");
|
|
27
|
+
try {
|
|
28
|
+
writeFileSync(socketPath, "stale");
|
|
29
|
+
removeIpcEndpointFile(socketPath);
|
|
30
|
+
expect(existsSync(socketPath)).toBe(false);
|
|
31
|
+
removeIpcEndpointFile("\\\\.\\pipe\\vellum-assistant-test");
|
|
32
|
+
} finally {
|
|
33
|
+
rmSync(dir, { recursive: true, force: true });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { unlinkSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { posix, win32 } from "node:path";
|
|
5
|
+
|
|
6
|
+
const DARWIN_UNIX_SOCKET_MAX_PATH_BYTES = 103;
|
|
7
|
+
const DEFAULT_UNIX_SOCKET_MAX_PATH_BYTES = 107;
|
|
8
|
+
const IPC_TMP_DIR_NAME = "vellum-ipc";
|
|
9
|
+
const join = posix.join;
|
|
10
|
+
|
|
11
|
+
export type IpcSocketPathSource =
|
|
12
|
+
| "env-override"
|
|
13
|
+
| "workspace"
|
|
14
|
+
| "tmp-hash"
|
|
15
|
+
| "tmp-short-hash"
|
|
16
|
+
| "windows-named-pipe";
|
|
17
|
+
|
|
18
|
+
export interface IpcSocketPathResolution {
|
|
19
|
+
path: string;
|
|
20
|
+
source: IpcSocketPathSource;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface IpcEndpointOptions {
|
|
24
|
+
workspaceDir: string;
|
|
25
|
+
env?: Record<string, string | undefined>;
|
|
26
|
+
platform?: NodeJS.Platform;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getUnixSocketMaxPathBytes(platform: NodeJS.Platform): number {
|
|
30
|
+
return platform === "darwin"
|
|
31
|
+
? DARWIN_UNIX_SOCKET_MAX_PATH_BYTES
|
|
32
|
+
: DEFAULT_UNIX_SOCKET_MAX_PATH_BYTES;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isPathWithinSocketLimit(path: string, maxPathBytes: number): boolean {
|
|
36
|
+
return Buffer.byteLength(path, "utf8") <= maxPathBytes;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Derive the env var name and socket filename from a socket name.
|
|
41
|
+
*
|
|
42
|
+
* Examples (hyphens in the name become underscores in the env var):
|
|
43
|
+
*/
|
|
44
|
+
function deriveSocketNames(socketName: string): {
|
|
45
|
+
envVar: string;
|
|
46
|
+
fileName: string;
|
|
47
|
+
} {
|
|
48
|
+
if (!/^[a-z][a-z0-9-]{0,63}$/.test(socketName)) {
|
|
49
|
+
throw new Error(`Invalid IPC endpoint name: ${socketName}`);
|
|
50
|
+
}
|
|
51
|
+
const envVar = `${socketName.toUpperCase().replace(/-/g, "_")}_IPC_SOCKET_DIR`;
|
|
52
|
+
const fileName = `${socketName}.sock`;
|
|
53
|
+
return { envVar, fileName };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function resolveWindowsNamedPipe(
|
|
57
|
+
socketName: string,
|
|
58
|
+
workspaceDir: string,
|
|
59
|
+
): IpcSocketPathResolution {
|
|
60
|
+
const workspaceIdentity = win32
|
|
61
|
+
.normalize(workspaceDir)
|
|
62
|
+
.replace(/\\+$/, "")
|
|
63
|
+
.toLowerCase();
|
|
64
|
+
const hash = createHash("sha256")
|
|
65
|
+
.update(`${workspaceIdentity}\0${socketName}`)
|
|
66
|
+
.digest("hex")
|
|
67
|
+
.slice(0, 24);
|
|
68
|
+
return {
|
|
69
|
+
path: `\\\\.\\pipe\\vellum-${socketName}-${hash}`,
|
|
70
|
+
source: "windows-named-pipe",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function isNamedPipePath(endpointPath: string): boolean {
|
|
75
|
+
return /^\\\\[.?]\\pipe\\/.test(endpointPath);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function removeIpcEndpointFile(endpointPath: string): void {
|
|
79
|
+
if (isNamedPipePath(endpointPath)) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
unlinkSync(endpointPath);
|
|
84
|
+
} catch {
|
|
85
|
+
// Already absent.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Resolve the path to an IPC socket file.
|
|
91
|
+
*
|
|
92
|
+
* Resolution order:
|
|
93
|
+
* POSIX uses overrides, workspace paths, then bounded temporary paths.
|
|
94
|
+
* Windows uses deterministic named pipes.
|
|
95
|
+
*/
|
|
96
|
+
export function resolveIpcEndpoint(
|
|
97
|
+
socketName: string,
|
|
98
|
+
options: IpcEndpointOptions,
|
|
99
|
+
): IpcSocketPathResolution {
|
|
100
|
+
const { envVar, fileName } = deriveSocketNames(socketName);
|
|
101
|
+
const platform = options.platform ?? process.platform;
|
|
102
|
+
if (platform === "win32") {
|
|
103
|
+
return resolveWindowsNamedPipe(socketName, options.workspaceDir);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Explicit override via env var.
|
|
107
|
+
const envSocketDir = (options.env ?? process.env)[envVar]?.trim();
|
|
108
|
+
if (envSocketDir) {
|
|
109
|
+
return {
|
|
110
|
+
path: join(envSocketDir, fileName),
|
|
111
|
+
source: "env-override",
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const maxPathBytes = getUnixSocketMaxPathBytes(platform);
|
|
116
|
+
const workspacePath = join(options.workspaceDir, fileName);
|
|
117
|
+
|
|
118
|
+
if (isPathWithinSocketLimit(workspacePath, maxPathBytes)) {
|
|
119
|
+
return {
|
|
120
|
+
path: workspacePath,
|
|
121
|
+
source: "workspace",
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Workspace path exceeds AF_UNIX limit - fall back to tmpdir.
|
|
126
|
+
const hash = createHash("sha256")
|
|
127
|
+
.update(workspacePath)
|
|
128
|
+
.digest("hex")
|
|
129
|
+
.slice(0, 12);
|
|
130
|
+
const hashedPath = join(tmpdir(), IPC_TMP_DIR_NAME, `${hash}-${fileName}`);
|
|
131
|
+
if (isPathWithinSocketLimit(hashedPath, maxPathBytes)) {
|
|
132
|
+
return {
|
|
133
|
+
path: hashedPath,
|
|
134
|
+
source: "tmp-hash",
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
path: join(tmpdir(), `v-${hash}.sock`),
|
|
140
|
+
source: "tmp-short-hash",
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -4,6 +4,8 @@ export {
|
|
|
4
4
|
type SocketWatchdogOptions,
|
|
5
5
|
type SocketWatchdogLogger,
|
|
6
6
|
} from "./socket-watchdog.js";
|
|
7
|
+
export { isNamedPipePath, removeIpcEndpointFile, resolveIpcEndpoint } from "./endpoint.js";
|
|
8
|
+
export { ipcListenOptions } from "./listen-options.js";
|
|
7
9
|
export {
|
|
8
10
|
IpcFrameReader,
|
|
9
11
|
writeLegacyMessage,
|
|
@@ -46,6 +46,7 @@ interface TestHarness {
|
|
|
46
46
|
|
|
47
47
|
interface BuildOptions {
|
|
48
48
|
intervalMs?: number;
|
|
49
|
+
socketPathOverride?: string;
|
|
49
50
|
createServerOverride?: () => Server;
|
|
50
51
|
/** Override `getServer` to simulate races. */
|
|
51
52
|
getServerOverride?: () => Server | null;
|
|
@@ -75,7 +76,7 @@ function buildHarness(opts: BuildOptions): TestHarness {
|
|
|
75
76
|
};
|
|
76
77
|
|
|
77
78
|
const watchdog = new SocketWatchdog({
|
|
78
|
-
socketPath,
|
|
79
|
+
socketPath: opts.socketPathOverride ?? socketPath,
|
|
79
80
|
intervalMs: opts.intervalMs ?? 0,
|
|
80
81
|
getServer: opts.getServerOverride ?? (() => serverRef.current),
|
|
81
82
|
createServer: opts.createServerOverride ?? defaultFactory,
|
|
@@ -190,6 +191,19 @@ describe("SocketWatchdog", () => {
|
|
|
190
191
|
expect(harness.rebinds).toHaveLength(0);
|
|
191
192
|
});
|
|
192
193
|
|
|
194
|
+
test("named pipes skip filesystem watchdog behavior", async () => {
|
|
195
|
+
const server = createServer();
|
|
196
|
+
harness = buildHarness({
|
|
197
|
+
socketPathOverride: "\\\\.\\pipe\\vellum-assistant-test",
|
|
198
|
+
getServerOverride: () => server,
|
|
199
|
+
createServerOverride: () => {
|
|
200
|
+
throw new Error("must not rebind");
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
harness.spawnedServers.push(server);
|
|
204
|
+
expect(await harness.watchdog.rebindIfMissing()).toBe(false);
|
|
205
|
+
});
|
|
206
|
+
|
|
193
207
|
test("rebindIfMissing recreates the listener when the path is gone", async () => {
|
|
194
208
|
harness = buildHarness({});
|
|
195
209
|
const initial = await startInitialServer(harness);
|
|
@@ -16,6 +16,9 @@ import { existsSync, mkdirSync, unlinkSync } from "node:fs";
|
|
|
16
16
|
import type { Server } from "node:net";
|
|
17
17
|
import { dirname } from "node:path";
|
|
18
18
|
|
|
19
|
+
import { isNamedPipePath } from "./endpoint.js";
|
|
20
|
+
import { ipcListenOptions } from "./listen-options.js";
|
|
21
|
+
|
|
19
22
|
/**
|
|
20
23
|
* Minimal logger surface (pino-compatible). Each method receives a context
|
|
21
24
|
* object plus an optional human-readable message.
|
|
@@ -66,6 +69,9 @@ const DEFAULT_INTERVAL_MS = 5000;
|
|
|
66
69
|
* `mkdir` only applies the mode to directories it creates.
|
|
67
70
|
*/
|
|
68
71
|
export function ensureSocketDir(socketPath: string): void {
|
|
72
|
+
if (isNamedPipePath(socketPath)) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
69
75
|
const socketDir = dirname(socketPath);
|
|
70
76
|
if (!existsSync(socketDir)) {
|
|
71
77
|
mkdirSync(socketDir, { recursive: true, mode: 0o700 });
|
|
@@ -109,7 +115,13 @@ export class SocketWatchdog {
|
|
|
109
115
|
* watchdog is already running.
|
|
110
116
|
*/
|
|
111
117
|
start(): void {
|
|
112
|
-
if (
|
|
118
|
+
if (
|
|
119
|
+
this.intervalMs <= 0 ||
|
|
120
|
+
this.handle !== null ||
|
|
121
|
+
isNamedPipePath(this.socketPath)
|
|
122
|
+
) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
113
125
|
this.handle = setInterval(() => {
|
|
114
126
|
// The async entry path of rebindIfMissing performs filesystem work
|
|
115
127
|
// (ensureSocketDir, createServer) before its inner try/catch, so a
|
|
@@ -143,6 +155,9 @@ export class SocketWatchdog {
|
|
|
143
155
|
* is not running, or a shutdown/restart raced the rebind.
|
|
144
156
|
*/
|
|
145
157
|
async rebindIfMissing(): Promise<boolean> {
|
|
158
|
+
if (isNamedPipePath(this.socketPath)) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
146
161
|
const initialServer = this.getServer();
|
|
147
162
|
if (initialServer === null) return false;
|
|
148
163
|
if (existsSync(this.socketPath)) return false;
|
|
@@ -167,7 +182,7 @@ export class SocketWatchdog {
|
|
|
167
182
|
};
|
|
168
183
|
newServer.once("error", onError);
|
|
169
184
|
newServer.once("listening", onListening);
|
|
170
|
-
newServer.listen(this.socketPath);
|
|
185
|
+
newServer.listen(ipcListenOptions(this.socketPath));
|
|
171
186
|
});
|
|
172
187
|
} catch (err) {
|
|
173
188
|
this.log.error(
|
|
@@ -21,6 +21,12 @@
|
|
|
21
21
|
* `Date#toISOString()`.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Pairing-challenge TTL in milliseconds (10 minutes): the gateway's challenge
|
|
26
|
+
* store enforces it and the `vellum pair` CLI renders it in user-facing copy.
|
|
27
|
+
*/
|
|
28
|
+
export const REMOTE_WEB_PAIRING_CODE_TTL_MS = 10 * 60 * 1000;
|
|
29
|
+
|
|
24
30
|
/** `POST /v1/remote-web/pairing-challenge` request body. */
|
|
25
31
|
export interface RemoteWebPairingChallengeRequest {
|
|
26
32
|
/** Public https base URL the scanning device can reach the assistant at. */
|
package/openapi.yaml
CHANGED
|
@@ -6772,6 +6772,9 @@ paths:
|
|
|
6772
6772
|
- messageId
|
|
6773
6773
|
- title
|
|
6774
6774
|
additionalProperties: false
|
|
6775
|
+
historyOrphaned:
|
|
6776
|
+
type: boolean
|
|
6777
|
+
const: true
|
|
6775
6778
|
archivedAt:
|
|
6776
6779
|
type: number
|
|
6777
6780
|
surfacedAt:
|
|
@@ -7132,6 +7135,9 @@ paths:
|
|
|
7132
7135
|
- messageId
|
|
7133
7136
|
- title
|
|
7134
7137
|
additionalProperties: false
|
|
7138
|
+
historyOrphaned:
|
|
7139
|
+
type: boolean
|
|
7140
|
+
const: true
|
|
7135
7141
|
archivedAt:
|
|
7136
7142
|
type: number
|
|
7137
7143
|
surfacedAt:
|
|
@@ -8342,6 +8348,9 @@ paths:
|
|
|
8342
8348
|
- messageId
|
|
8343
8349
|
- title
|
|
8344
8350
|
additionalProperties: false
|
|
8351
|
+
historyOrphaned:
|
|
8352
|
+
type: boolean
|
|
8353
|
+
const: true
|
|
8345
8354
|
archivedAt:
|
|
8346
8355
|
type: number
|
|
8347
8356
|
surfacedAt:
|
|
@@ -20067,8 +20076,8 @@ paths:
|
|
|
20067
20076
|
type: string
|
|
20068
20077
|
clientOs:
|
|
20069
20078
|
description:
|
|
20070
|
-
Client OS surface ("web" | "ios" | "macos" | "android"), reported separately from `interface`.
|
|
20071
|
-
per-turn `client_os` context only; does not affect transport/host-proxy capabilities.
|
|
20079
|
+
Client OS surface ("web" | "ios" | "macos" | "windows" | "android"), reported separately from `interface`.
|
|
20080
|
+
Drives the per-turn `client_os` context only; does not affect transport/host-proxy capabilities.
|
|
20072
20081
|
type: string
|
|
20073
20082
|
visibleAppId:
|
|
20074
20083
|
description:
|
|
@@ -25219,8 +25228,9 @@ paths:
|
|
|
25219
25228
|
per conversation after activity (time/message thresholds, pre-compaction) — there is no global schedule, so
|
|
25220
25229
|
`nextRunAt` is always null and no run-now endpoint exists. `intervalMs` is the per-conversation time threshold
|
|
25221
25230
|
(`memory.retrospective.timeThresholdMs`), `lastRunAt` is the `createdAt` of the most recent retrospective
|
|
25222
|
-
conversation across both legacy and fork sources,
|
|
25223
|
-
`memory.v2.enabled`).
|
|
25231
|
+
conversation across both legacy and fork sources, `available` gates on `memory.enabled` (not
|
|
25232
|
+
`memory.v2.enabled`), and `enabled` narrows that by `memory.retrospective.enabled`, the per-pass kill switch
|
|
25233
|
+
that stops retrospectives without disabling the rest of memory.
|
|
25224
25234
|
tags:
|
|
25225
25235
|
- retrospective
|
|
25226
25236
|
responses:
|
|
@@ -29405,6 +29415,73 @@ paths:
|
|
|
29405
29415
|
- isBinary
|
|
29406
29416
|
- content
|
|
29407
29417
|
additionalProperties: false
|
|
29418
|
+
/v1/skills/{id}/history:
|
|
29419
|
+
get:
|
|
29420
|
+
operationId: skills_by_id_history_get
|
|
29421
|
+
summary: Get skill revision history
|
|
29422
|
+
description:
|
|
29423
|
+
"Return a skill's recent updates, newest first, each with a combined diff across the skill directory.
|
|
29424
|
+
Read-only: revisions come from the workspace git repository, which already retains prior content. Commits whose
|
|
29425
|
+
only in-skill change is the `lastUsedAt` usage stamp are omitted, so entries are edits rather than loads."
|
|
29426
|
+
tags:
|
|
29427
|
+
- skills
|
|
29428
|
+
parameters:
|
|
29429
|
+
- name: id
|
|
29430
|
+
in: path
|
|
29431
|
+
required: true
|
|
29432
|
+
schema:
|
|
29433
|
+
type: string
|
|
29434
|
+
- name: limit
|
|
29435
|
+
in: query
|
|
29436
|
+
required: false
|
|
29437
|
+
schema:
|
|
29438
|
+
type: integer
|
|
29439
|
+
description: Maximum revisions to return, newest first. Defaults to 20 and is clamped to 100.
|
|
29440
|
+
responses:
|
|
29441
|
+
"200":
|
|
29442
|
+
description: Successful response
|
|
29443
|
+
content:
|
|
29444
|
+
application/json:
|
|
29445
|
+
schema:
|
|
29446
|
+
type: object
|
|
29447
|
+
properties:
|
|
29448
|
+
skillId:
|
|
29449
|
+
type: string
|
|
29450
|
+
description: The skill these revisions belong to
|
|
29451
|
+
revisions:
|
|
29452
|
+
type: array
|
|
29453
|
+
items:
|
|
29454
|
+
type: object
|
|
29455
|
+
properties:
|
|
29456
|
+
id:
|
|
29457
|
+
type: string
|
|
29458
|
+
description: Opaque revision identifier
|
|
29459
|
+
changedAt:
|
|
29460
|
+
type: string
|
|
29461
|
+
description: ISO-8601 time of the update
|
|
29462
|
+
files:
|
|
29463
|
+
type: array
|
|
29464
|
+
items:
|
|
29465
|
+
type: string
|
|
29466
|
+
description: Paths changed, relative to the skill directory
|
|
29467
|
+
diff:
|
|
29468
|
+
type: string
|
|
29469
|
+
description: Unified diff of this update, scoped to the skill
|
|
29470
|
+
required:
|
|
29471
|
+
- id
|
|
29472
|
+
- changedAt
|
|
29473
|
+
- files
|
|
29474
|
+
- diff
|
|
29475
|
+
additionalProperties: false
|
|
29476
|
+
description: Recent updates, newest first
|
|
29477
|
+
truncatedByCompaction:
|
|
29478
|
+
type: boolean
|
|
29479
|
+
description: Older history was squashed away, so the oldest entry is a floor rather than the skill's creation
|
|
29480
|
+
required:
|
|
29481
|
+
- skillId
|
|
29482
|
+
- revisions
|
|
29483
|
+
- truncatedByCompaction
|
|
29484
|
+
additionalProperties: false
|
|
29408
29485
|
/v1/skills/{id}/inspect:
|
|
29409
29486
|
get:
|
|
29410
29487
|
operationId: skills_by_id_inspect_get
|
package/package.json
CHANGED
|
@@ -88,6 +88,7 @@ function makeTarget(): Conversation {
|
|
|
88
88
|
getTurnChannelContext: () => null,
|
|
89
89
|
getTurnInterfaceContext: () => null,
|
|
90
90
|
drainQueue: async () => {},
|
|
91
|
+
kickDrainQueue: async () => {},
|
|
91
92
|
// Pre-run auto-compaction gate — no-op for these tests.
|
|
92
93
|
maybeCompact: async () => null,
|
|
93
94
|
};
|
|
@@ -100,6 +100,7 @@ function makeTarget(): {
|
|
|
100
100
|
getTurnChannelContext: () => null,
|
|
101
101
|
getTurnInterfaceContext: () => null,
|
|
102
102
|
drainQueue: async () => {},
|
|
103
|
+
kickDrainQueue: async () => {},
|
|
103
104
|
// Pre-run auto-compaction gate — no-op for these tests.
|
|
104
105
|
maybeCompact: async () => null,
|
|
105
106
|
buildCurrentSystemPrompt: () => "mock-system-prompt",
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The canned-reply routes (canned first greeting, unknown slash command) hold
|
|
3
|
+
* the processing lock themselves and release it from a deferred callback that
|
|
4
|
+
* also publishes their client event burst. That release is the only one those
|
|
5
|
+
* turns get: no agent loop runs, so a throw escaping the burst would latch the
|
|
6
|
+
* conversation "processing" for the life of the daemon and queue every later
|
|
7
|
+
* send behind an idle conversation.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, test } from "bun:test";
|
|
10
|
+
|
|
11
|
+
import type { QueueDrainReason } from "../daemon/conversation-queue-manager.js";
|
|
12
|
+
import { scheduleCannedReplyRelease } from "../runtime/routes/canned-reply-release.js";
|
|
13
|
+
|
|
14
|
+
function makeConversation() {
|
|
15
|
+
let processing = true;
|
|
16
|
+
const drainOrigins: string[] = [];
|
|
17
|
+
return {
|
|
18
|
+
isProcessing: () => processing,
|
|
19
|
+
drainOrigins,
|
|
20
|
+
target: {
|
|
21
|
+
setProcessing: (value: boolean) => {
|
|
22
|
+
processing = value;
|
|
23
|
+
},
|
|
24
|
+
kickDrainQueue: (_reason?: QueueDrainReason, origin?: string) => {
|
|
25
|
+
drainOrigins.push(origin ?? "");
|
|
26
|
+
return Promise.resolve();
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Let the scheduled `setTimeout(..., 0)` callback run. */
|
|
33
|
+
function nextTick(): Promise<void> {
|
|
34
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("scheduleCannedReplyRelease", () => {
|
|
38
|
+
test("releases and drains after the deferred event burst", async () => {
|
|
39
|
+
const conversation = makeConversation();
|
|
40
|
+
const order: string[] = [];
|
|
41
|
+
|
|
42
|
+
scheduleCannedReplyRelease({
|
|
43
|
+
conversation: {
|
|
44
|
+
setProcessing: (value: boolean) => {
|
|
45
|
+
order.push("release");
|
|
46
|
+
conversation.target.setProcessing(value);
|
|
47
|
+
},
|
|
48
|
+
kickDrainQueue: conversation.target.kickDrainQueue,
|
|
49
|
+
},
|
|
50
|
+
origin: "canned_greeting",
|
|
51
|
+
emit: () => {
|
|
52
|
+
order.push("emit");
|
|
53
|
+
},
|
|
54
|
+
afterRelease: () => {
|
|
55
|
+
order.push("after");
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Nothing runs synchronously: the HTTP response has to reach the client
|
|
60
|
+
// before its events do.
|
|
61
|
+
expect(order).toEqual([]);
|
|
62
|
+
|
|
63
|
+
await nextTick();
|
|
64
|
+
|
|
65
|
+
expect(order).toEqual(["emit", "release", "after"]);
|
|
66
|
+
expect(conversation.isProcessing()).toBe(false);
|
|
67
|
+
expect(conversation.drainOrigins).toEqual(["canned_greeting"]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("still releases and drains when a broadcast throws", async () => {
|
|
71
|
+
const conversation = makeConversation();
|
|
72
|
+
|
|
73
|
+
scheduleCannedReplyRelease({
|
|
74
|
+
conversation: conversation.target,
|
|
75
|
+
origin: "slash_command",
|
|
76
|
+
emit: () => {
|
|
77
|
+
throw new Error("broadcast exploded");
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await nextTick();
|
|
82
|
+
|
|
83
|
+
expect(conversation.isProcessing()).toBe(false);
|
|
84
|
+
expect(conversation.drainOrigins).toEqual(["slash_command"]);
|
|
85
|
+
});
|
|
86
|
+
});
|