@vellumai/assistant 0.7.3 → 0.8.0
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 +29 -28
- package/Dockerfile +1 -0
- package/__tests__/permissions/gateway-threshold-reader.test.ts +236 -9
- package/bun.lock +3 -0
- package/knip.json +1 -0
- package/node_modules/@vellumai/ipc-server-utils/bun.lock +24 -0
- package/node_modules/@vellumai/ipc-server-utils/package.json +18 -0
- package/node_modules/@vellumai/ipc-server-utils/src/index.ts +6 -0
- package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +430 -0
- package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +221 -0
- package/node_modules/@vellumai/ipc-server-utils/tsconfig.json +20 -0
- package/openapi.yaml +22 -4
- package/package.json +3 -1
- package/src/__tests__/annotate-risk-options.test.ts +291 -0
- package/src/__tests__/approval-cascade.test.ts +8 -16
- package/src/__tests__/approval-routes-http.test.ts +6 -0
- package/src/__tests__/auto-analysis-end-to-end.test.ts +12 -25
- package/src/__tests__/call-constants.test.ts +10 -1
- package/src/__tests__/call-controller.test.ts +127 -0
- package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +58 -28
- package/src/__tests__/config-loader-platform-defaults.test.ts +284 -1
- package/src/__tests__/context-search-memory-source.test.ts +3 -26
- package/src/__tests__/context-search-pkb-source.test.ts +12 -6
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -6
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop.test.ts +3 -3
- package/src/__tests__/conversation-confirmation-signals.test.ts +5 -13
- package/src/__tests__/conversation-init.benchmark.test.ts +1 -1
- package/src/__tests__/conversation-process-callsite.test.ts +1 -6
- package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -6
- package/src/__tests__/conversation-runtime-assembly.test.ts +15 -6
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -6
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +170 -9
- package/src/__tests__/conversation-surfaces-data-persist.test.ts +73 -1
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +59 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -7
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -7
- package/src/__tests__/filing-service.test.ts +2 -19
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +10 -26
- package/src/__tests__/injector-chain.test.ts +24 -16
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +10 -7
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +154 -67
- package/src/__tests__/notification-decision-fallback.test.ts +91 -0
- package/src/__tests__/notification-decision-strategy.test.ts +22 -0
- package/src/__tests__/oauth-cli.test.ts +121 -0
- package/src/__tests__/relay-server.test.ts +46 -2
- package/src/__tests__/secret-prompt-log-hygiene.test.ts +7 -5
- package/src/__tests__/secret-prompter-channel-fallback.test.ts +7 -5
- package/src/__tests__/secret-response-routing.test.ts +7 -5
- package/src/__tests__/server-history-render.test.ts +82 -0
- package/src/__tests__/skill-include-graph.test.ts +31 -0
- package/src/__tests__/skill-load-tool.test.ts +44 -16
- package/src/__tests__/skills.test.ts +39 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -42
- package/src/__tests__/tool-executor.test.ts +155 -0
- package/src/__tests__/voice-session-bridge.test.ts +3 -0
- package/src/__tests__/workspace-migration-069-seed-onboarding-threads.test.ts +120 -0
- package/src/__tests__/workspace-migration-071-remove-safe-storage-release-note.test.ts +206 -0
- package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +15 -27
- package/src/agent/loop.ts +11 -0
- package/src/approvals/guardian-decision-primitive.ts +0 -13
- package/src/approvals/guardian-request-resolvers.ts +4 -32
- package/src/calls/call-constants.ts +5 -8
- package/src/calls/call-controller.ts +130 -67
- package/src/calls/relay-server.ts +7 -1
- package/src/calls/voice-session-bridge.ts +1 -1
- package/src/cli/commands/memory-v2.ts +7 -7
- package/src/cli/commands/oauth/__tests__/connect.test.ts +0 -254
- package/src/cli/commands/oauth/connect.ts +10 -52
- package/src/config/bundled-skills/app-builder/SKILL.md +1 -3
- package/src/config/feature-flag-registry.json +1 -17
- package/src/config/loader.ts +72 -19
- package/src/config/schemas/memory-v2.ts +1 -1
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +32 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +32 -0
- package/src/daemon/conversation-agent-loop.ts +13 -10
- package/src/daemon/conversation-lifecycle.ts +22 -8
- package/src/daemon/conversation-surfaces.ts +16 -14
- package/src/daemon/conversation-tool-setup.ts +9 -5
- package/src/daemon/conversation.ts +1 -1
- package/src/daemon/handlers/shared.ts +26 -0
- package/src/daemon/host-bash-proxy.ts +1 -1
- package/src/daemon/host-browser-proxy.ts +1 -1
- package/src/daemon/host-cu-proxy.ts +1 -1
- package/src/daemon/host-file-proxy.ts +1 -1
- package/src/daemon/host-transfer-proxy.ts +2 -2
- package/src/daemon/lifecycle.ts +88 -73
- package/src/daemon/memory-v2-startup.ts +55 -14
- package/src/daemon/message-types/messages.ts +19 -1
- package/src/documents/document-store.ts +35 -1
- package/src/filing/filing-service.ts +2 -3
- package/src/heartbeat/heartbeat-service.ts +1 -1
- package/src/ipc/assistant-server.ts +93 -36
- package/src/ipc/skill-server.ts +99 -42
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +10 -57
- package/src/memory/context-search/sources/memory-v2.ts +1 -17
- package/src/memory/context-search/sources/memory.ts +2 -2
- package/src/memory/context-search/sources/pkb.ts +2 -3
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +104 -61
- package/src/memory/graph/__tests__/handle-remember-v2.test.ts +11 -26
- package/src/memory/graph/conversation-graph-memory.ts +32 -9
- package/src/memory/graph/graph-search.test.ts +6 -5
- package/src/memory/graph/graph-search.ts +3 -4
- package/src/memory/graph/retriever.test.ts +12 -7
- package/src/memory/graph/retriever.ts +4 -5
- package/src/memory/graph/tool-handlers.ts +3 -4
- package/src/memory/graph/tools.ts +4 -4
- package/src/memory/indexer.ts +1 -2
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +116 -0
- package/src/memory/jobs/embed-concept-page.ts +223 -87
- package/src/memory/jobs-worker.ts +8 -4
- package/src/memory/pkb/pkb-search.test.ts +6 -5
- package/src/memory/pkb/pkb-search.ts +4 -5
- package/src/memory/qdrant-client.ts +3 -0
- package/src/memory/search/semantic.ts +4 -5
- package/src/memory/v2/__tests__/activation.test.ts +35 -5
- package/src/memory/v2/__tests__/consolidation-job.test.ts +21 -32
- package/src/memory/v2/__tests__/injection.test.ts +140 -23
- package/src/memory/v2/__tests__/qdrant.test.ts +310 -9
- package/src/memory/v2/__tests__/sim.test.ts +118 -7
- package/src/memory/v2/__tests__/static-context.test.ts +1 -13
- package/src/memory/v2/__tests__/sweep-job.test.ts +19 -33
- package/src/memory/v2/consolidation-job.ts +7 -8
- package/src/memory/v2/injection.ts +32 -12
- package/src/memory/v2/page-store.ts +39 -0
- package/src/memory/v2/prompts/consolidation.ts +5 -0
- package/src/memory/v2/qdrant.ts +209 -48
- package/src/memory/v2/sim.ts +67 -26
- package/src/memory/v2/static-context.ts +4 -8
- package/src/memory/v2/sweep-job.ts +5 -6
- package/src/memory/v2/types.ts +7 -0
- package/src/notifications/copy-composer.ts +46 -12
- package/src/notifications/decision-engine.ts +46 -0
- package/src/permissions/gateway-threshold-reader.ts +116 -8
- package/src/permissions/prompter.ts +86 -96
- package/src/permissions/secret-prompter.ts +31 -31
- package/src/plugins/defaults/injectors.ts +1 -2
- package/src/proactive-artifact/job.test.ts +51 -4
- package/src/proactive-artifact/job.ts +16 -2
- package/src/proactive-artifact/message-copy.ts +18 -1
- package/src/prompts/templates/SOUL.md +13 -28
- package/src/runtime/auth/route-policy.ts +1 -0
- package/src/runtime/channel-approvals.ts +3 -2
- package/src/runtime/guardian-reply-router.ts +0 -10
- package/src/runtime/pending-interactions.ts +19 -15
- package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +147 -0
- package/src/runtime/routes/approval-routes.ts +7 -3
- package/src/runtime/routes/consolidation-routes.ts +8 -9
- package/src/runtime/routes/conversation-query-routes.ts +44 -1
- package/src/runtime/routes/debug-bash-routes.ts +2 -0
- package/src/runtime/routes/filing-routes.ts +2 -3
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +0 -3
- package/src/runtime/routes/memory-item-routes.test.ts +3 -9
- package/src/runtime/routes/memory-item-routes.ts +5 -6
- package/src/runtime/routes/memory-v2-routes.ts +103 -17
- package/src/skills/include-graph.ts +35 -13
- package/src/tools/document/document-tool.ts +20 -0
- package/src/tools/executor.ts +18 -2
- package/src/tools/memory/register.test.ts +7 -5
- package/src/tools/permission-checker.ts +15 -0
- package/src/tools/skills/load.ts +24 -20
- package/src/tools/tool-name-aliases.ts +19 -0
- package/src/tools/types.ts +19 -1
- package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +4 -62
- package/src/workspace/migrations/069-seed-onboarding-threads.ts +28 -0
- package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +31 -0
- package/src/workspace/migrations/071-remove-safe-storage-release-note.ts +111 -0
- package/src/workspace/migrations/registry.ts +6 -0
package/ARCHITECTURE.md
CHANGED
|
@@ -1278,7 +1278,7 @@ graph TB
|
|
|
1278
1278
|
- Managed-store writes are atomic (tmp file + rename) to prevent partial `SKILL.md` or `SKILLS.md` files.
|
|
1279
1279
|
- After persist or delete, the file watcher triggers conversation eviction; the next turn runs in a fresh conversation. The model's system prompt instructs it to continue normally.
|
|
1280
1280
|
- macOS UI shows Inspect and Delete controls for managed skills only (source = "managed").
|
|
1281
|
-
- `skill_load`
|
|
1281
|
+
- `skill_load` resolves the recursive include graph (via `include-graph.ts`) before emitting output. Missing children are listed as suggested skills without child `<loaded_skill>` markers; cycles still produce `isError: true` with no marker. Valid includes produce an "Included Skills (immediate)" metadata section showing child ID, name, description, and path.
|
|
1282
1282
|
|
|
1283
1283
|
### Skills Authoring via HTTP
|
|
1284
1284
|
|
|
@@ -1289,32 +1289,33 @@ The Skills page in the macOS client can author managed skills through the daemon
|
|
|
1289
1289
|
|
|
1290
1290
|
### Include Graph Validation
|
|
1291
1291
|
|
|
1292
|
-
Skills can declare child relationships via the `includes` frontmatter field (a JSON array of skill IDs). When `skill_load` loads a parent skill, it
|
|
1292
|
+
Skills can declare child relationships via the `includes` frontmatter field (a JSON array of skill IDs). When `skill_load` loads a parent skill, it attempts to resolve and auto-install missing includes before emitting output. Available includes are appended to the loaded skill output; unavailable includes are surfaced as suggestions instead of blocking the parent skill.
|
|
1293
1293
|
|
|
1294
1294
|
```mermaid
|
|
1295
1295
|
graph LR
|
|
1296
1296
|
LOAD["skill_load(parent)"] --> CATALOG["loadSkillCatalog()"]
|
|
1297
1297
|
CATALOG --> INDEX["indexCatalogById()"]
|
|
1298
|
-
INDEX -->
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1298
|
+
INDEX --> AUTOINSTALL["Attempt catalog auto-install<br/>for missing includes"]
|
|
1299
|
+
AUTOINSTALL --> RESOLVE["collectAllMissing(rootId, index)<br/>+ validateIncludeCycles(rootId, index)"]
|
|
1300
|
+
RESOLVE -->|"ok + no missing child"| OUTPUT["Emit output +<br/>Included Skills (immediate)<br/>+ loaded_skill markers"]
|
|
1301
|
+
RESOLVE -->|"ok + missing child"| OUTPUT_MISSING["Emit parent output +<br/>Suggested Included Skills<br/>without child markers"]
|
|
1302
|
+
RESOLVE -->|"cycle detected"| ERR_CYCLE["isError: true<br/>no loaded_skill marker"]
|
|
1302
1303
|
```
|
|
1303
1304
|
|
|
1304
1305
|
**Validation rules:**
|
|
1305
1306
|
|
|
1306
|
-
- **Missing children**:
|
|
1307
|
+
- **Missing children**: Missing includes trigger catalog auto-install attempts. Any include still unavailable is listed under "Suggested Included Skills (not loaded)" and does not receive a `<loaded_skill>` marker.
|
|
1307
1308
|
- **Cycles**: Three-state DFS (unseen → visiting → done) detects direct and indirect cycles. The error includes the cycle path.
|
|
1308
|
-
- **Fail-closed**:
|
|
1309
|
+
- **Fail-closed cycles**: Circular include chains still return `isError: true` with no `<loaded_skill>` marker.
|
|
1309
1310
|
|
|
1310
|
-
**Key constraint**: Include metadata is
|
|
1311
|
+
**Key constraint**: Include metadata is advisory. Available included skills are appended to the parent output and receive explicit `<loaded_skill>` markers; unavailable included skills remain suggestions so the agent can search for and install them if the task needs their guidance or tools.
|
|
1311
1312
|
|
|
1312
|
-
| Source File | Purpose
|
|
1313
|
-
| --------------------------------------- |
|
|
1314
|
-
| `assistant/src/skills/include-graph.ts` | `indexCatalogById()`, `getImmediateChildren()`, `validateIncludes()`, `traverseIncludes()` |
|
|
1315
|
-
| `assistant/src/tools/skills/load.ts` | Include
|
|
1316
|
-
| `assistant/src/config/skills.ts` | `includes` field parsing from SKILL.md frontmatter
|
|
1317
|
-
| `assistant/src/skills/managed-store.ts` | `includes` emission in `buildSkillMarkdown()`
|
|
1313
|
+
| Source File | Purpose |
|
|
1314
|
+
| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
1315
|
+
| `assistant/src/skills/include-graph.ts` | `indexCatalogById()`, `getImmediateChildren()`, `validateIncludes()`, `validateIncludeCycles()`, `traverseIncludes()` |
|
|
1316
|
+
| `assistant/src/tools/skills/load.ts` | Include resolution integration in `skill_load` execute path |
|
|
1317
|
+
| `assistant/src/config/skills.ts` | `includes` field parsing from SKILL.md frontmatter |
|
|
1318
|
+
| `assistant/src/skills/managed-store.ts` | `includes` emission in `buildSkillMarkdown()` |
|
|
1318
1319
|
|
|
1319
1320
|
---
|
|
1320
1321
|
|
|
@@ -1553,19 +1554,19 @@ Every layer in the pipeline defaults to rejection rather than silent degradation
|
|
|
1553
1554
|
|
|
1554
1555
|
### Key Source Files
|
|
1555
1556
|
|
|
1556
|
-
| File | Role
|
|
1557
|
-
| --------------------------------------------------- |
|
|
1558
|
-
| `assistant/src/config/skills.ts` | Skill catalog loading: bundled, managed, workspace, extra directories
|
|
1559
|
-
| `assistant/src/config/bundled-skills/` | Bundled skill directories (browser, gmail, computer-use, weather, etc.)
|
|
1560
|
-
| `assistant/src/skills/tool-manifest.ts` | `TOOLS.json` parser and validator
|
|
1561
|
-
| `assistant/src/skills/active-skill-tools.ts` | `deriveActiveSkills()` — scans history for `<loaded_skill>` markers
|
|
1562
|
-
| `assistant/src/skills/include-graph.ts` | Include graph builder: `indexCatalogById()`, `validateIncludes()`,
|
|
1563
|
-
| `assistant/src/daemon/conversation-skill-tools.ts` | `projectSkillTools()` — per-turn projection, register/unregister lifecycle
|
|
1564
|
-
| `assistant/src/tools/skills/skill-tool-factory.ts` | `createSkillToolsFromManifest()` — manifest entries to Tool objects
|
|
1565
|
-
| `assistant/src/tools/skills/skill-script-runner.ts` | Host runner: dynamic import + `run()` call
|
|
1566
|
-
| `assistant/src/tools/skills/sandbox-runner.ts` | Sandbox runner: isolated subprocess execution
|
|
1567
|
-
| `assistant/src/tools/registry.ts` | `registerSkillTools()` / `unregisterSkillTools()` — global tool registry
|
|
1568
|
-
| `assistant/src/permissions/checker.ts` | Skill-origin default-ask permission policy
|
|
1557
|
+
| File | Role |
|
|
1558
|
+
| --------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
|
1559
|
+
| `assistant/src/config/skills.ts` | Skill catalog loading: bundled, managed, workspace, extra directories |
|
|
1560
|
+
| `assistant/src/config/bundled-skills/` | Bundled skill directories (browser, gmail, computer-use, weather, etc.) |
|
|
1561
|
+
| `assistant/src/skills/tool-manifest.ts` | `TOOLS.json` parser and validator |
|
|
1562
|
+
| `assistant/src/skills/active-skill-tools.ts` | `deriveActiveSkills()` — scans history for `<loaded_skill>` markers |
|
|
1563
|
+
| `assistant/src/skills/include-graph.ts` | Include graph builder: `indexCatalogById()`, `validateIncludes()`, `validateIncludeCycles()` |
|
|
1564
|
+
| `assistant/src/daemon/conversation-skill-tools.ts` | `projectSkillTools()` — per-turn projection, register/unregister lifecycle |
|
|
1565
|
+
| `assistant/src/tools/skills/skill-tool-factory.ts` | `createSkillToolsFromManifest()` — manifest entries to Tool objects |
|
|
1566
|
+
| `assistant/src/tools/skills/skill-script-runner.ts` | Host runner: dynamic import + `run()` call |
|
|
1567
|
+
| `assistant/src/tools/skills/sandbox-runner.ts` | Sandbox runner: isolated subprocess execution |
|
|
1568
|
+
| `assistant/src/tools/registry.ts` | `registerSkillTools()` / `unregisterSkillTools()` — global tool registry |
|
|
1569
|
+
| `assistant/src/permissions/checker.ts` | Skill-origin default-ask permission policy |
|
|
1569
1570
|
|
|
1570
1571
|
---
|
|
1571
1572
|
|
package/Dockerfile
CHANGED
|
@@ -22,6 +22,7 @@ COPY packages/service-contracts ./packages/service-contracts
|
|
|
22
22
|
COPY packages/credential-storage ./packages/credential-storage
|
|
23
23
|
COPY packages/egress-proxy ./packages/egress-proxy
|
|
24
24
|
COPY packages/gateway-client ./packages/gateway-client
|
|
25
|
+
COPY packages/ipc-server-utils ./packages/ipc-server-utils
|
|
25
26
|
COPY packages/skill-host-contracts ./packages/skill-host-contracts
|
|
26
27
|
COPY packages/slack-text ./packages/slack-text
|
|
27
28
|
COPY packages/twilio-client ./packages/twilio-client
|
|
@@ -31,18 +31,70 @@ mock.module("../../src/ipc/gateway-client.js", () => ({
|
|
|
31
31
|
},
|
|
32
32
|
}));
|
|
33
33
|
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
34
|
+
// Capture logger output so coalescing tests can assert on it. Existing
|
|
35
|
+
// tests don't read the array, so capturing is invisible to them.
|
|
36
|
+
//
|
|
37
|
+
// Bun's `mock.module("../../src/util/logger.js", ...)` does not intercept
|
|
38
|
+
// transitive imports (see comment in stt-hints.test.ts and avatar-e2e.test.ts).
|
|
39
|
+
// Mocking `pino` at the package level works because getLogger uses pino
|
|
40
|
+
// child loggers under the hood — intercepting pino captures everything.
|
|
41
|
+
interface LogCall {
|
|
42
|
+
level: "warn" | "info" | "error" | "debug";
|
|
43
|
+
fields: Record<string, unknown>;
|
|
44
|
+
message: string;
|
|
45
|
+
}
|
|
46
|
+
const logCalls: LogCall[] = [];
|
|
47
|
+
|
|
48
|
+
function makeLogFn(level: LogCall["level"]) {
|
|
49
|
+
return (
|
|
50
|
+
fieldsOrMsg: Record<string, unknown> | string,
|
|
51
|
+
maybeMsg?: string,
|
|
52
|
+
) => {
|
|
53
|
+
if (typeof fieldsOrMsg === "string") {
|
|
54
|
+
logCalls.push({ level, fields: {}, message: fieldsOrMsg });
|
|
55
|
+
} else {
|
|
56
|
+
logCalls.push({
|
|
57
|
+
level,
|
|
58
|
+
fields: fieldsOrMsg,
|
|
59
|
+
message: maybeMsg ?? "",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const mockChildLogger = {
|
|
66
|
+
debug: () => {},
|
|
67
|
+
info: makeLogFn("info"),
|
|
68
|
+
warn: makeLogFn("warn"),
|
|
69
|
+
error: makeLogFn("error"),
|
|
70
|
+
fatal: () => {},
|
|
71
|
+
trace: () => {},
|
|
72
|
+
silent: () => {},
|
|
73
|
+
// pino loggers are themselves callable as a no-op shorthand; child() returns
|
|
74
|
+
// another logger.
|
|
75
|
+
child(): typeof mockChildLogger {
|
|
76
|
+
return mockChildLogger;
|
|
77
|
+
},
|
|
78
|
+
bindings: () => ({}),
|
|
79
|
+
level: "info",
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const mockPinoLogger = Object.assign(() => mockChildLogger, {
|
|
83
|
+
destination: () => ({}),
|
|
84
|
+
multistream: () => ({}),
|
|
85
|
+
stdTimeFunctions: { isoTime: () => "" },
|
|
86
|
+
stdSerializers: {},
|
|
87
|
+
symbols: {},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
mock.module("pino", () => ({ default: mockPinoLogger }));
|
|
91
|
+
mock.module("pino-pretty", () => ({ default: () => ({}) }));
|
|
43
92
|
|
|
44
93
|
import {
|
|
45
94
|
_clearGlobalCacheForTesting,
|
|
95
|
+
_getFailureStateForTesting,
|
|
96
|
+
_resetFailureCoalesceForTesting,
|
|
97
|
+
_setFailureWarnIntervalForTesting,
|
|
46
98
|
getAutoApproveThreshold,
|
|
47
99
|
} from "../../src/permissions/gateway-threshold-reader.js";
|
|
48
100
|
|
|
@@ -51,7 +103,9 @@ import {
|
|
|
51
103
|
function resetMocks(): void {
|
|
52
104
|
ipcCallLog.length = 0;
|
|
53
105
|
ipcHandler = () => undefined;
|
|
106
|
+
logCalls.length = 0;
|
|
54
107
|
_clearGlobalCacheForTesting();
|
|
108
|
+
_resetFailureCoalesceForTesting();
|
|
55
109
|
}
|
|
56
110
|
|
|
57
111
|
afterEach(resetMocks);
|
|
@@ -221,3 +275,176 @@ describe("getAutoApproveThreshold", () => {
|
|
|
221
275
|
expect(ipcCallLog).toEqual(["/v1/permissions/thresholds"]);
|
|
222
276
|
});
|
|
223
277
|
});
|
|
278
|
+
|
|
279
|
+
// ── Failure coalescing ───────────────────────────────────────────────────────
|
|
280
|
+
|
|
281
|
+
describe("failure-coalescing log behavior", () => {
|
|
282
|
+
test("first failure WARNs immediately and starts a streak", async () => {
|
|
283
|
+
ipcHandler = () => {
|
|
284
|
+
throw new Error("Connection refused");
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
expect(await getAutoApproveThreshold(undefined, "background")).toBe("none");
|
|
288
|
+
|
|
289
|
+
const warns = logCalls.filter((c) => c.level === "warn");
|
|
290
|
+
expect(warns.length).toBe(1);
|
|
291
|
+
expect(warns[0]?.fields).toMatchObject({
|
|
292
|
+
op: "global_thresholds",
|
|
293
|
+
consecutiveFailures: 1,
|
|
294
|
+
event: "ipc_threshold_failure",
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const state = _getFailureStateForTesting("global_thresholds");
|
|
298
|
+
expect(state).toBeDefined();
|
|
299
|
+
expect(state?.consecutiveFailures).toBe(1);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("subsequent failures within the WARN window do not log but still increment state", async () => {
|
|
303
|
+
// 1-hour window so the test never accidentally crosses it.
|
|
304
|
+
_setFailureWarnIntervalForTesting(60 * 60 * 1000);
|
|
305
|
+
ipcHandler = () => {
|
|
306
|
+
throw new Error("ENOENT");
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
for (let i = 0; i < 100; i++) {
|
|
310
|
+
_clearGlobalCacheForTesting(); // force re-fetch each call
|
|
311
|
+
await getAutoApproveThreshold(undefined, "background");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const warns = logCalls.filter((c) => c.level === "warn");
|
|
315
|
+
// At most one WARN — the very first call. All 99 follow-ups suppressed.
|
|
316
|
+
expect(warns.length).toBe(1);
|
|
317
|
+
|
|
318
|
+
const state = _getFailureStateForTesting("global_thresholds");
|
|
319
|
+
expect(state?.consecutiveFailures).toBe(100);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("a fresh WARN fires once the cadence window elapses", async () => {
|
|
323
|
+
// 5ms window so the test runs fast.
|
|
324
|
+
_setFailureWarnIntervalForTesting(5);
|
|
325
|
+
ipcHandler = () => {
|
|
326
|
+
throw new Error("ENOENT");
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
await getAutoApproveThreshold(undefined, "background");
|
|
330
|
+
expect(logCalls.filter((c) => c.level === "warn").length).toBe(1);
|
|
331
|
+
|
|
332
|
+
// Wait past the window then fail again.
|
|
333
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
334
|
+
_clearGlobalCacheForTesting();
|
|
335
|
+
await getAutoApproveThreshold(undefined, "background");
|
|
336
|
+
|
|
337
|
+
const warns = logCalls.filter((c) => c.level === "warn");
|
|
338
|
+
expect(warns.length).toBe(2);
|
|
339
|
+
// Second WARN includes the streak metadata so dashboards can see how
|
|
340
|
+
// many failures were swallowed in between.
|
|
341
|
+
expect(warns[1]?.fields).toMatchObject({
|
|
342
|
+
op: "global_thresholds",
|
|
343
|
+
consecutiveFailures: 2,
|
|
344
|
+
event: "ipc_threshold_failure",
|
|
345
|
+
});
|
|
346
|
+
expect(warns[1]?.fields.streakDurationMs).toBeDefined();
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test("recovery emits an INFO with the swallowed-failure count and clears state", async () => {
|
|
350
|
+
_setFailureWarnIntervalForTesting(60 * 60 * 1000);
|
|
351
|
+
let working = false;
|
|
352
|
+
ipcHandler = (method) => {
|
|
353
|
+
if (working && method === "get_global_thresholds") {
|
|
354
|
+
return { interactive: "medium", autonomous: "low" };
|
|
355
|
+
}
|
|
356
|
+
throw new Error("ENOENT");
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
// Three failures, then it recovers.
|
|
360
|
+
for (let i = 0; i < 3; i++) {
|
|
361
|
+
_clearGlobalCacheForTesting();
|
|
362
|
+
await getAutoApproveThreshold(undefined, "background");
|
|
363
|
+
}
|
|
364
|
+
expect(_getFailureStateForTesting("global_thresholds")?.consecutiveFailures).toBe(
|
|
365
|
+
3,
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
working = true;
|
|
369
|
+
_clearGlobalCacheForTesting();
|
|
370
|
+
expect(await getAutoApproveThreshold(undefined, "background")).toBe("low");
|
|
371
|
+
|
|
372
|
+
const infos = logCalls.filter((c) => c.level === "info");
|
|
373
|
+
expect(infos.length).toBe(1);
|
|
374
|
+
expect(infos[0]?.fields).toMatchObject({
|
|
375
|
+
op: "global_thresholds",
|
|
376
|
+
swallowedFailures: 3,
|
|
377
|
+
event: "ipc_threshold_recovered",
|
|
378
|
+
});
|
|
379
|
+
expect(infos[0]?.fields.streakDurationMs).toBeDefined();
|
|
380
|
+
|
|
381
|
+
expect(_getFailureStateForTesting("global_thresholds")).toBeUndefined();
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("conversation and global ops have independent failure streaks", async () => {
|
|
385
|
+
_setFailureWarnIntervalForTesting(60 * 60 * 1000);
|
|
386
|
+
// conversation IPC fails (transport — returns undefined), global IPC works.
|
|
387
|
+
ipcHandler = (method) => {
|
|
388
|
+
if (method === "get_conversation_threshold") return undefined;
|
|
389
|
+
if (method === "get_global_thresholds") {
|
|
390
|
+
return { interactive: "medium", autonomous: "low" };
|
|
391
|
+
}
|
|
392
|
+
return undefined;
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
// First call: conversation transport fails, global succeeds.
|
|
396
|
+
expect(await getAutoApproveThreshold("conv-1", "conversation")).toBe(
|
|
397
|
+
"medium",
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
expect(
|
|
401
|
+
_getFailureStateForTesting("conversation_threshold")?.consecutiveFailures,
|
|
402
|
+
).toBe(1);
|
|
403
|
+
expect(_getFailureStateForTesting("global_thresholds")).toBeUndefined();
|
|
404
|
+
|
|
405
|
+
const warns = logCalls.filter((c) => c.level === "warn");
|
|
406
|
+
expect(warns.length).toBe(1);
|
|
407
|
+
expect(warns[0]?.fields.op).toBe("conversation_threshold");
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test("a successful conversation override clears the conversation streak even when the gateway returns null (no override)", async () => {
|
|
411
|
+
_setFailureWarnIntervalForTesting(60 * 60 * 1000);
|
|
412
|
+
|
|
413
|
+
// First two calls: conversation IPC returns undefined (transport failure).
|
|
414
|
+
let working = false;
|
|
415
|
+
ipcHandler = (method) => {
|
|
416
|
+
if (method === "get_conversation_threshold") {
|
|
417
|
+
return working ? null : undefined;
|
|
418
|
+
}
|
|
419
|
+
if (method === "get_global_thresholds") {
|
|
420
|
+
return { interactive: "low", autonomous: "none" };
|
|
421
|
+
}
|
|
422
|
+
return undefined;
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
// Force two transport failures.
|
|
426
|
+
await getAutoApproveThreshold("conv-2", "conversation");
|
|
427
|
+
await new Promise((r) => setTimeout(r, 6)); // bypass the 5s convo cache
|
|
428
|
+
_clearGlobalCacheForTesting();
|
|
429
|
+
// Convo cache is keyed on conversationId — change the id to bypass.
|
|
430
|
+
await getAutoApproveThreshold("conv-3", "conversation");
|
|
431
|
+
expect(
|
|
432
|
+
_getFailureStateForTesting("conversation_threshold")?.consecutiveFailures,
|
|
433
|
+
).toBe(2);
|
|
434
|
+
|
|
435
|
+
// Now the IPC starts working — even a null "no override" response is a
|
|
436
|
+
// successful round-trip and must clear the streak.
|
|
437
|
+
working = true;
|
|
438
|
+
_clearGlobalCacheForTesting();
|
|
439
|
+
await getAutoApproveThreshold("conv-4", "conversation");
|
|
440
|
+
|
|
441
|
+
const infos = logCalls.filter((c) => c.level === "info");
|
|
442
|
+
expect(infos.length).toBe(1);
|
|
443
|
+
expect(infos[0]?.fields).toMatchObject({
|
|
444
|
+
op: "conversation_threshold",
|
|
445
|
+
swallowedFailures: 2,
|
|
446
|
+
event: "ipc_threshold_recovered",
|
|
447
|
+
});
|
|
448
|
+
expect(_getFailureStateForTesting("conversation_threshold")).toBeUndefined();
|
|
449
|
+
});
|
|
450
|
+
});
|
package/bun.lock
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"@vellumai/credential-storage": "file:../packages/credential-storage",
|
|
19
19
|
"@vellumai/egress-proxy": "file:../packages/egress-proxy",
|
|
20
20
|
"@vellumai/gateway-client": "file:../packages/gateway-client",
|
|
21
|
+
"@vellumai/ipc-server-utils": "file:../packages/ipc-server-utils",
|
|
21
22
|
"@vellumai/service-contracts": "file:../packages/service-contracts",
|
|
22
23
|
"@vellumai/skill-host-contracts": "file:../packages/skill-host-contracts",
|
|
23
24
|
"@vellumai/slack-text": "file:../packages/slack-text",
|
|
@@ -421,6 +422,8 @@
|
|
|
421
422
|
|
|
422
423
|
"@vellumai/gateway-client": ["@vellumai/gateway-client@file:../packages/gateway-client", { "dependencies": { "@vellumai/service-contracts": "file:../service-contracts" }, "devDependencies": { "@types/bun": "1.3.10", "typescript": "5.9.3" } }],
|
|
423
424
|
|
|
425
|
+
"@vellumai/ipc-server-utils": ["@vellumai/ipc-server-utils@file:../packages/ipc-server-utils", { "devDependencies": { "@types/bun": "1.3.10", "typescript": "5.9.3" } }],
|
|
426
|
+
|
|
424
427
|
"@vellumai/service-contracts": ["@vellumai/service-contracts@file:../packages/service-contracts", { "dependencies": { "zod": "4.3.6" }, "devDependencies": { "@types/bun": "1.2.4", "typescript": "5.7.3" } }],
|
|
425
428
|
|
|
426
429
|
"@vellumai/skill-host-contracts": ["@vellumai/skill-host-contracts@file:../packages/skill-host-contracts", { "devDependencies": { "@types/bun": "1.3.10", "typescript": "5.9.3" } }],
|
package/knip.json
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"configVersion": 1,
|
|
4
|
+
"workspaces": {
|
|
5
|
+
"": {
|
|
6
|
+
"name": "@vellumai/ipc-server-utils",
|
|
7
|
+
"devDependencies": {
|
|
8
|
+
"@types/bun": "1.3.10",
|
|
9
|
+
"typescript": "5.9.3",
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
"packages": {
|
|
14
|
+
"@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="],
|
|
15
|
+
|
|
16
|
+
"@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
|
|
17
|
+
|
|
18
|
+
"bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],
|
|
19
|
+
|
|
20
|
+
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
|
21
|
+
|
|
22
|
+
"undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vellumai/ipc-server-utils",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"typecheck": "bunx tsc --noEmit",
|
|
12
|
+
"test": "bun test src/"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/bun": "1.3.10",
|
|
16
|
+
"typescript": "5.9.3"
|
|
17
|
+
}
|
|
18
|
+
}
|