@vellumai/assistant 0.8.9-staging.2 → 0.8.9-staging.4
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/docs/activation-funnel-telemetry.md +310 -0
- package/openapi.yaml +16 -115
- package/package.json +1 -1
- package/src/__tests__/activation-early-marking.test.ts +120 -0
- package/src/__tests__/agent-loop-output-hooks.test.ts +13 -13
- package/src/__tests__/anthropic-provider.test.ts +23 -8
- package/src/__tests__/approval-cascade.test.ts +1 -1
- package/src/__tests__/compaction-direct.test.ts +32 -18
- package/src/__tests__/compaction-events.test.ts +2 -2
- package/src/__tests__/compaction.benchmark.test.ts +1 -1
- package/src/__tests__/context-overflow-reducer.test.ts +5 -5
- package/src/__tests__/context-window-manager-compact-retry.test.ts +121 -15
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
- package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
- package/src/__tests__/conversation-error.test.ts +15 -1
- package/src/__tests__/conversation-history-web-search.test.ts +5 -0
- package/src/__tests__/conversation-media-retry.test.ts +1 -1
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +40 -0
- package/src/__tests__/conversation-process-callsite.test.ts +1 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
- package/src/__tests__/conversation-queue.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +71 -0
- package/src/__tests__/conversation-slash-queue.test.ts +1 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
- package/src/__tests__/conversation-speed-override.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +395 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +44 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +73 -4
- package/src/__tests__/conversation-undo.test.ts +2 -2
- package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/cu-unified-flow.test.ts +36 -0
- package/src/__tests__/history-repair-hook.test.ts +2 -0
- package/src/__tests__/llm-resolver.test.ts +73 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +1 -2
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +145 -0
- package/src/__tests__/persist-unsendable-image.test.ts +97 -1
- package/src/__tests__/plugin-external-api.test.ts +68 -0
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +69 -0
- package/src/__tests__/published-app-updater.test.ts +138 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +5 -7
- package/src/__tests__/title-generate-hook.test.ts +2 -0
- package/src/__tests__/web-fetch.test.ts +45 -0
- package/src/acp/__tests__/helpers/acp-config-stub.ts +0 -2
- package/src/acp/resolve-agent.test.ts +0 -56
- package/src/acp/resolve-agent.ts +10 -38
- package/src/agent/loop.ts +13 -27
- package/src/api/responses/memory-v3-selection-log.ts +19 -10
- package/src/cli/commands/__tests__/memory-v3.test.ts +191 -210
- package/src/cli/commands/memory-v3.ts +57 -199
- package/src/cli/lib/__tests__/install-from-github.test.ts +232 -29
- package/src/cli/lib/__tests__/plugin-details.test.ts +28 -19
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +57 -7
- package/src/cli/lib/__tests__/search-plugins.test.ts +17 -10
- package/src/cli/lib/install-from-github.ts +258 -41
- package/src/cli/lib/plugin-details.ts +20 -13
- package/src/cli/lib/plugin-marketplace.ts +23 -5
- package/src/cli/lib/search-plugins.ts +14 -8
- package/src/config/acp-defaults.ts +3 -3
- package/src/config/acp-schema.ts +1 -7
- package/src/config/bundled-skills/acp/SKILL.md +4 -17
- package/src/config/bundled-skills/acp/TOOLS.json +2 -2
- package/src/config/call-site-defaults.ts +0 -1
- package/src/config/feature-flag-registry.json +3 -18
- package/src/config/llm-resolver.ts +39 -7
- package/src/config/schemas/__tests__/memory-v3.test.ts +25 -9
- package/src/config/schemas/call-site-catalog.ts +0 -7
- package/src/config/schemas/llm.ts +17 -1
- package/src/config/schemas/memory-v3.ts +58 -8
- package/src/config/seed-inference-profiles.ts +18 -0
- package/src/context/post-turn-tool-result-truncation.ts +39 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +8 -1
- package/src/daemon/conversation-agent-loop.ts +87 -95
- package/src/daemon/conversation-error.ts +31 -4
- package/src/daemon/conversation-history.ts +1 -1
- package/src/daemon/conversation-media-retry.ts +19 -6
- package/src/daemon/conversation-messaging.ts +17 -0
- package/src/daemon/conversation-process.ts +14 -5
- package/src/daemon/conversation-queue-manager.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +37 -1
- package/src/daemon/conversation-surfaces.ts +141 -3
- package/src/daemon/conversation.ts +48 -13
- package/src/daemon/external-plugins-bootstrap.ts +8 -3
- package/src/daemon/persist-unsendable-image.ts +62 -25
- package/src/daemon/process-message.ts +1 -1
- package/src/daemon/tool-side-effects.ts +15 -0
- package/src/memory/__tests__/activation-session-store.test.ts +41 -0
- package/src/memory/__tests__/onboarding-events-store.test.ts +80 -0
- package/src/memory/activation-session-store.ts +43 -0
- package/src/memory/db-init.ts +4 -0
- package/src/memory/migrations/273-onboarding-events-funnel-columns.ts +46 -0
- package/src/memory/migrations/274-create-activation-sessions.ts +15 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/onboarding-events-store.ts +66 -18
- package/src/memory/schema/infrastructure.ts +13 -0
- package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -97
- package/src/memory/v2/__tests__/page-store.test.ts +22 -0
- package/src/memory/v2/consolidation-job.ts +2 -72
- package/src/memory/v2/types.ts +5 -0
- package/src/messaging/providers/telegram-bot/api.ts +14 -5
- package/src/notifications/adapters/telegram.ts +7 -1
- package/src/plugin-api/constants.ts +2 -2
- package/src/plugin-api/index.ts +2 -2
- package/src/plugin-api/types.ts +19 -5
- package/src/plugins/defaults/compaction/compact.ts +24 -15
- package/src/plugins/defaults/compaction/context-overflow-reducer.ts +4 -4
- package/src/plugins/defaults/compaction/manager-store.ts +1 -1
- package/src/{context → plugins/defaults/compaction}/window-manager.ts +68 -12
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +12 -18
- package/src/plugins/defaults/memory-v3-shadow/__tests__/capabilities.test.ts +19 -66
- package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +181 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/edge.test.ts +247 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +139 -129
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +332 -164
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +516 -293
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +306 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +51 -21
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +402 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-needle.test.ts +135 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/sections.test.ts +125 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +66 -11
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +446 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +271 -110
- package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +0 -22
- package/src/plugins/defaults/memory-v3-shadow/capabilities.ts +26 -62
- package/src/plugins/defaults/memory-v3-shadow/dense.ts +97 -0
- package/src/plugins/defaults/memory-v3-shadow/edge.ts +252 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +3 -2
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +415 -181
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +196 -56
- package/src/plugins/defaults/memory-v3-shadow/page-content.ts +39 -2
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +204 -0
- package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +15 -7
- package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +236 -0
- package/src/plugins/defaults/memory-v3-shadow/section-needle.ts +200 -0
- package/src/plugins/defaults/memory-v3-shadow/sections.ts +115 -0
- package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +75 -3
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +111 -78
- package/src/plugins/defaults/memory-v3-shadow/types.ts +52 -19
- package/src/plugins/defaults/memory-v3-shadow/working-set.ts +4 -1
- package/src/plugins/external-api.ts +114 -0
- package/src/prompts/system-prompt.ts +61 -10
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +37 -2
- package/src/providers/anthropic/client.ts +9 -10
- package/src/providers/inference/kimi-cjk-token-ids.ts +493 -0
- package/src/providers/inference/logit-bias.ts +55 -0
- package/src/providers/openai/__tests__/vision-not-supported.test.ts +75 -0
- package/src/providers/openai/chat-completions-provider.ts +44 -1
- package/src/providers/retry.ts +22 -0
- package/src/providers/types.ts +6 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +112 -0
- package/src/runtime/routes/acp-routes.test.ts +3 -20
- package/src/runtime/routes/app-management-routes.ts +3 -0
- package/src/runtime/routes/conversation-routes.ts +2 -0
- package/src/runtime/routes/memory-v3-routes.ts +64 -314
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +1 -1
- package/src/runtime/routes/stt-routes.ts +45 -12
- package/src/runtime/routes/workspace-routes.ts +50 -15
- package/src/services/published-app-updater.ts +30 -8
- package/src/telemetry/__tests__/activation-funnel.test.ts +95 -0
- package/src/telemetry/activation-funnel.ts +167 -0
- package/src/telemetry/types.ts +13 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +154 -0
- package/src/telemetry/usage-telemetry-reporter.ts +26 -1
- package/src/tools/acp/list-agents.test.ts +2 -18
- package/src/tools/acp/list-agents.ts +3 -15
- package/src/tools/acp/spawn.test.ts +0 -10
- package/src/tools/browser/browser-execution.ts +12 -2
- package/src/tools/network/web-fetch.ts +65 -24
- package/src/tools/skills/load.ts +1 -1
- package/src/tools/ui-surface/definitions.ts +7 -0
- package/src/acp/feature-gate.test.ts +0 -48
- package/src/acp/feature-gate.ts +0 -34
- package/src/plugins/defaults/memory-v3-shadow/__tests__/assign.test.ts +0 -242
- package/src/plugins/defaults/memory-v3-shadow/__tests__/core.test.ts +0 -39
- package/src/plugins/defaults/memory-v3-shadow/__tests__/health.test.ts +0 -219
- package/src/plugins/defaults/memory-v3-shadow/__tests__/needle.test.ts +0 -107
- package/src/plugins/defaults/memory-v3-shadow/__tests__/provider-blocks.test.ts +0 -13
- package/src/plugins/defaults/memory-v3-shadow/__tests__/reconcile.test.ts +0 -274
- package/src/plugins/defaults/memory-v3-shadow/__tests__/router.test.ts +0 -337
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selector.test.ts +0 -470
- package/src/plugins/defaults/memory-v3-shadow/__tests__/snapshot.test.ts +0 -168
- package/src/plugins/defaults/memory-v3-shadow/__tests__/tree.test.ts +0 -192
- package/src/plugins/defaults/memory-v3-shadow/assign.ts +0 -272
- package/src/plugins/defaults/memory-v3-shadow/core.ts +0 -26
- package/src/plugins/defaults/memory-v3-shadow/health.ts +0 -0
- package/src/plugins/defaults/memory-v3-shadow/needle.ts +0 -115
- package/src/plugins/defaults/memory-v3-shadow/provider-blocks.ts +0 -26
- package/src/plugins/defaults/memory-v3-shadow/reconcile.ts +0 -527
- package/src/plugins/defaults/memory-v3-shadow/router.ts +0 -190
- package/src/plugins/defaults/memory-v3-shadow/selector.ts +0 -226
- package/src/plugins/defaults/memory-v3-shadow/snapshot.ts +0 -209
- package/src/plugins/defaults/memory-v3-shadow/tree.ts +0 -174
- package/src/util/map-limit.ts +0 -27
|
@@ -1,278 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Memory v3 route definitions —
|
|
3
|
-
*
|
|
2
|
+
* Memory v3 route definitions — live-lane maintenance operations for the
|
|
3
|
+
* section-lane memory model.
|
|
4
4
|
*
|
|
5
|
-
* The daemon owns the live
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* write. The read-only `health` report and the `set-core` cost preview also
|
|
9
|
-
* route through the daemon so it stays the single source of truth for the live
|
|
10
|
-
* workspace tree.
|
|
5
|
+
* The daemon owns the live shadow lanes, so the maintenance verbs run here,
|
|
6
|
+
* inside the daemon process, where the in-memory lanes can be invalidated or
|
|
7
|
+
* rebuilt after a write:
|
|
11
8
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
9
|
+
* - `rebuild-index` — drop the cached lanes so the next turn rebuilds.
|
|
10
|
+
* - `backfill-sections` — one-time full embed of every page's sections into
|
|
11
|
+
* the dense store, advancing the maintain high-water mark.
|
|
12
|
+
*
|
|
13
|
+
* Each route's behavior lives in a small DI-friendly `handle*` function (with an
|
|
14
|
+
* injectable config seam) so tests can drive it without mocking module globals.
|
|
15
|
+
* The exported `RouteDefinition`s are thin adapters over those handlers.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { writeFile } from "node:fs/promises";
|
|
19
|
-
import { join } from "node:path";
|
|
20
|
-
|
|
21
18
|
import { z } from "zod";
|
|
22
19
|
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
computeV3Health,
|
|
27
|
-
renderV3Health,
|
|
28
|
-
} from "../../plugins/defaults/memory-v3-shadow/health.js";
|
|
29
|
-
import {
|
|
30
|
-
type LeafRef,
|
|
31
|
-
reconcileTree,
|
|
32
|
-
} from "../../plugins/defaults/memory-v3-shadow/reconcile.js";
|
|
20
|
+
import { getConfig } from "../../config/loader.js";
|
|
21
|
+
import type { AssistantConfig } from "../../config/types.js";
|
|
22
|
+
import { backfillAllSections } from "../../plugins/defaults/memory-v3-shadow/maintain-job.js";
|
|
33
23
|
import { invalidateLanes } from "../../plugins/defaults/memory-v3-shadow/shadow-plugin.js";
|
|
34
|
-
import {
|
|
35
|
-
coreSlugs,
|
|
36
|
-
loadLeafTree,
|
|
37
|
-
resolveDataDir,
|
|
38
|
-
} from "../../plugins/defaults/memory-v3-shadow/tree.js";
|
|
39
|
-
import type {
|
|
40
|
-
LeafPath,
|
|
41
|
-
LeafTree,
|
|
42
|
-
Slug,
|
|
43
|
-
} from "../../plugins/defaults/memory-v3-shadow/types.js";
|
|
44
24
|
import { getLogger } from "../../util/logger.js";
|
|
45
|
-
import { getWorkspaceDir } from "../../util/platform.js";
|
|
46
25
|
import { ACTOR_PRINCIPALS, type RoutePolicy } from "../auth/route-policy.js";
|
|
47
|
-
import {
|
|
48
|
-
import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
|
|
26
|
+
import type { RouteDefinition } from "./types.js";
|
|
49
27
|
|
|
50
28
|
const log = getLogger("memory-v3-routes");
|
|
51
29
|
|
|
52
|
-
/**
|
|
53
|
-
* Filesystem location overrides. Production callers omit these and the handlers
|
|
54
|
-
* resolve the live workspace; tests inject a temp workspace + data dir to
|
|
55
|
-
* exercise the handlers without mocking module globals.
|
|
56
|
-
*/
|
|
57
|
-
export interface MemoryV3Deps {
|
|
58
|
-
dataDir?: string;
|
|
59
|
-
workspaceDir?: string;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// ---------------------------------------------------------------------------
|
|
63
|
-
// Shared loading helpers
|
|
64
|
-
// ---------------------------------------------------------------------------
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Load the live leaf tree + its full slug universe.
|
|
68
|
-
*
|
|
69
|
-
* Page `leaves:` frontmatter is the authoritative source of page→leaf
|
|
70
|
-
* membership: we build a `pageLeaves` map from the page index and union it over
|
|
71
|
-
* `assignments.json` via `loadLeafTree(dataDir, pageLeaves)`, and derive the
|
|
72
|
-
* slug universe (`allSlugs`) from the page index itself. This mirrors
|
|
73
|
-
* `maybePrependV3Health` in `memory/v2/consolidation-job.ts` so the CLI
|
|
74
|
-
* `health` / `set-core` cost computations agree with the consolidation-injected
|
|
75
|
-
* health block instead of disagreeing by reading stale assignments.json only.
|
|
76
|
-
*/
|
|
77
|
-
async function loadTreeAndSlugs(deps?: MemoryV3Deps): Promise<{
|
|
78
|
-
dataDir: string;
|
|
79
|
-
tree: LeafTree;
|
|
80
|
-
allSlugs: Slug[];
|
|
81
|
-
}> {
|
|
82
|
-
const workspaceDir = deps?.workspaceDir ?? getWorkspaceDir();
|
|
83
|
-
const dataDir = deps?.dataDir ?? resolveDataDir();
|
|
84
|
-
|
|
85
|
-
// Each page contributes its `leaves:` frontmatter as the authoritative leaf
|
|
86
|
-
// set for that slug; the slug universe is every page the index knows about.
|
|
87
|
-
const pageIndex = await getPageIndex(workspaceDir);
|
|
88
|
-
const pageLeaves = new Map<Slug, LeafPath[]>();
|
|
89
|
-
const allSlugs: Slug[] = [];
|
|
90
|
-
for (const entry of pageIndex.entries) {
|
|
91
|
-
pageLeaves.set(entry.slug, entry.leaves);
|
|
92
|
-
allSlugs.push(entry.slug);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const tree = await loadLeafTree(dataDir, pageLeaves);
|
|
96
|
-
return { dataDir, tree, allSlugs };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// ---------------------------------------------------------------------------
|
|
100
|
-
// health
|
|
101
|
-
// ---------------------------------------------------------------------------
|
|
102
|
-
|
|
103
|
-
const MemoryV3HealthResultSchema = z.object({
|
|
104
|
-
/** Pre-rendered, human-readable report. Empty string when all-green. */
|
|
105
|
-
rendered: z.string(),
|
|
106
|
-
/** The structural counts, for `--json` consumers. */
|
|
107
|
-
counts: z.object({
|
|
108
|
-
unassigned: z.number(),
|
|
109
|
-
danglingRefs: z.number(),
|
|
110
|
-
novelClusters: z.number(),
|
|
111
|
-
oversizedLeaves: z.number(),
|
|
112
|
-
tinyLeaves: z.number(),
|
|
113
|
-
}),
|
|
114
|
-
});
|
|
115
|
-
export type MemoryV3HealthResult = z.infer<typeof MemoryV3HealthResultSchema>;
|
|
116
|
-
|
|
117
|
-
export async function handleMemoryV3Health(
|
|
118
|
-
deps?: MemoryV3Deps,
|
|
119
|
-
): Promise<MemoryV3HealthResult> {
|
|
120
|
-
const { dataDir, tree, allSlugs } = await loadTreeAndSlugs(deps);
|
|
121
|
-
const core = await loadCore(dataDir);
|
|
122
|
-
const report = computeV3Health({ tree, allSlugs, core });
|
|
123
|
-
return {
|
|
124
|
-
rendered: renderV3Health(report),
|
|
125
|
-
counts: {
|
|
126
|
-
unassigned: report.unassigned.length,
|
|
127
|
-
danglingRefs: report.danglingRefs.length,
|
|
128
|
-
novelClusters: report.novelClusters.length,
|
|
129
|
-
oversizedLeaves: report.oversizedLeaves.length,
|
|
130
|
-
tinyLeaves: report.tinyLeaves.length,
|
|
131
|
-
},
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// ---------------------------------------------------------------------------
|
|
136
|
-
// set-core
|
|
137
|
-
// ---------------------------------------------------------------------------
|
|
138
|
-
|
|
139
|
-
const MemoryV3SetCoreBodySchema = z.object({
|
|
140
|
-
/** Leaves to add to the always-on core set. */
|
|
141
|
-
add: z.array(z.string()).optional(),
|
|
142
|
-
/** Leaves to remove from the always-on core set. */
|
|
143
|
-
remove: z.array(z.string()).optional(),
|
|
144
|
-
/**
|
|
145
|
-
* When true, persist the new core to `core.json` and invalidate the lanes.
|
|
146
|
-
* When false (default), compute the preview WITHOUT writing.
|
|
147
|
-
*/
|
|
148
|
-
write: z.boolean().optional(),
|
|
149
|
-
});
|
|
150
|
-
export type MemoryV3SetCoreBody = z.infer<typeof MemoryV3SetCoreBodySchema>;
|
|
151
|
-
|
|
152
|
-
const MemoryV3SetCoreResultSchema = z.object({
|
|
153
|
-
/** The core leaf set that would result (or did result, when `write`). */
|
|
154
|
-
nextCore: z.array(z.string()),
|
|
155
|
-
/** Number of unique page slugs the new core set pins always-on. */
|
|
156
|
-
alwaysOnPageCount: z.number(),
|
|
157
|
-
/** Whether `core.json` was written. */
|
|
158
|
-
written: z.boolean(),
|
|
159
|
-
});
|
|
160
|
-
export type MemoryV3SetCoreResult = z.infer<typeof MemoryV3SetCoreResultSchema>;
|
|
161
|
-
|
|
162
|
-
/** Wire-format error code for a `set-core` add referencing an unknown leaf. */
|
|
163
|
-
export const MEMORY_V3_UNKNOWN_LEAF_CODE = "MEMORY_V3_UNKNOWN_LEAF";
|
|
164
|
-
|
|
165
|
-
/** Thrown when a `set-core` add entry does not exist in the live tree. */
|
|
166
|
-
export class UnknownLeafError extends Error {
|
|
167
|
-
constructor(public readonly unknown: LeafPath[]) {
|
|
168
|
-
super(
|
|
169
|
-
`Unknown leaf path(s) — not present in the tree: ${unknown.join(", ")}`,
|
|
170
|
-
);
|
|
171
|
-
this.name = "UnknownLeafError";
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/** Persist the always-on core set back to `<dataDir>/core.json`. */
|
|
176
|
-
async function writeCore(dataDir: string, alwaysOn: LeafPath[]): Promise<void> {
|
|
177
|
-
await writeFile(
|
|
178
|
-
join(dataDir, "core.json"),
|
|
179
|
-
`${JSON.stringify({ alwaysOn }, null, 2)}\n`,
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
export async function handleMemoryV3SetCore(
|
|
184
|
-
body: MemoryV3SetCoreBody,
|
|
185
|
-
deps?: MemoryV3Deps,
|
|
186
|
-
): Promise<MemoryV3SetCoreResult> {
|
|
187
|
-
const add = body.add ?? [];
|
|
188
|
-
const remove = body.remove ?? [];
|
|
189
|
-
const { dataDir, tree } = await loadTreeAndSlugs(deps);
|
|
190
|
-
|
|
191
|
-
// Validate every ADD entry exists in the live tree. Removing an entry that is
|
|
192
|
-
// already absent is a no-op (idempotent), so only adds are validated.
|
|
193
|
-
const unknown = add.filter((leaf) => !tree.leaves.has(leaf));
|
|
194
|
-
if (unknown.length > 0) throw new UnknownLeafError(unknown);
|
|
195
|
-
|
|
196
|
-
const core = await loadCore(dataDir);
|
|
197
|
-
for (const leaf of remove) core.delete(leaf);
|
|
198
|
-
for (const leaf of add) core.add(leaf);
|
|
199
|
-
|
|
200
|
-
// Drop any pre-existing core entry that no longer maps to a live leaf, so the
|
|
201
|
-
// preview reflects what the tree can actually pin. Sorted for stable output.
|
|
202
|
-
const nextCore = [...core].filter((leaf) => tree.leaves.has(leaf)).sort();
|
|
203
|
-
const nextCoreSet = new Set<LeafPath>(nextCore);
|
|
204
|
-
|
|
205
|
-
// Cost preview: the number of UNIQUE page slugs the new core pins always-on.
|
|
206
|
-
const alwaysOnPageCount = coreSlugs(tree, nextCoreSet).size;
|
|
207
|
-
|
|
208
|
-
if (body.write === true) {
|
|
209
|
-
await writeCore(dataDir, nextCore);
|
|
210
|
-
invalidateLanes();
|
|
211
|
-
log.info(
|
|
212
|
-
{ coreSize: nextCore.length, alwaysOnPageCount },
|
|
213
|
-
"memory-v3 core updated",
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return { nextCore, alwaysOnPageCount, written: body.write === true };
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// ---------------------------------------------------------------------------
|
|
221
|
-
// reconcile
|
|
222
|
-
// ---------------------------------------------------------------------------
|
|
223
|
-
|
|
224
|
-
const MemoryV3ReconcileResultSchema = z.object({
|
|
225
|
-
renames: z.array(
|
|
226
|
-
z.object({
|
|
227
|
-
id: z.string().optional(),
|
|
228
|
-
oldPath: z.string(),
|
|
229
|
-
newPath: z.string(),
|
|
230
|
-
}),
|
|
231
|
-
),
|
|
232
|
-
deleted: z.array(z.string()),
|
|
233
|
-
prunedCore: z.array(z.string()),
|
|
234
|
-
});
|
|
235
|
-
export type MemoryV3ReconcileResult = z.infer<
|
|
236
|
-
typeof MemoryV3ReconcileResultSchema
|
|
237
|
-
>;
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Reconcile page + core references against the live on-disk tree.
|
|
241
|
-
*
|
|
242
|
-
* `prevLeaves` describes the tree as it was BEFORE the maintainer's on-disk
|
|
243
|
-
* restructuring. We derive it from the current leaf set, which makes reconcile
|
|
244
|
-
* a safe convergence pass: renames/deletes already applied on disk diff to
|
|
245
|
-
* nothing, while it still rewrites any page/core ref left dangling and prunes
|
|
246
|
-
* stale core entries (and fail-closed restores on a residual dangling ref).
|
|
247
|
-
* `reconcileTree` snapshots, applies, validates, and invalidates the lanes.
|
|
248
|
-
*/
|
|
249
|
-
export async function handleMemoryV3Reconcile(
|
|
250
|
-
deps?: MemoryV3Deps,
|
|
251
|
-
): Promise<MemoryV3ReconcileResult> {
|
|
252
|
-
const workspaceDir = deps?.workspaceDir ?? getWorkspaceDir();
|
|
253
|
-
const dataDir = deps?.dataDir ?? resolveDataDir();
|
|
254
|
-
// v1: prev == current is intentional. Without a captured prior leaf snapshot
|
|
255
|
-
// we cannot detect renames/moves/splits, so this runs as a convergence /
|
|
256
|
-
// dangling-ref prune pass. Full rename detection is a follow-up.
|
|
257
|
-
const prevLeaves = await loadPrevLeaves(dataDir);
|
|
258
|
-
|
|
259
|
-
const result = await reconcileTree({ prevLeaves, dataDir, workspaceDir });
|
|
260
|
-
return {
|
|
261
|
-
renames: result.renames,
|
|
262
|
-
deleted: result.deleted,
|
|
263
|
-
prunedCore: result.prunedCore,
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/** The current leaf set as `LeafRef`s (path + stable id when present). */
|
|
268
|
-
async function loadPrevLeaves(dataDir: string): Promise<LeafRef[]> {
|
|
269
|
-
const tree = await loadLeafTree(dataDir);
|
|
270
|
-
return [...tree.leaves.values()].map((node) => ({
|
|
271
|
-
path: node.path,
|
|
272
|
-
...(node.frontmatter.id ? { id: node.frontmatter.id } : {}),
|
|
273
|
-
}));
|
|
274
|
-
}
|
|
275
|
-
|
|
276
30
|
// ---------------------------------------------------------------------------
|
|
277
31
|
// rebuild-index
|
|
278
32
|
// ---------------------------------------------------------------------------
|
|
@@ -285,7 +39,7 @@ export type MemoryV3RebuildIndexResult = z.infer<
|
|
|
285
39
|
>;
|
|
286
40
|
|
|
287
41
|
/**
|
|
288
|
-
* Invalidate the v3 shadow lanes so the next turn rebuilds the
|
|
42
|
+
* Invalidate the v3 shadow lanes so the next turn rebuilds the section index
|
|
289
43
|
* from the current on-disk state. Runs in-daemon so it acts on the live
|
|
290
44
|
* process's cached lanes (an in-CLI call would invalidate nothing).
|
|
291
45
|
*/
|
|
@@ -296,21 +50,47 @@ export async function handleMemoryV3RebuildIndex(): Promise<MemoryV3RebuildIndex
|
|
|
296
50
|
}
|
|
297
51
|
|
|
298
52
|
// ---------------------------------------------------------------------------
|
|
299
|
-
//
|
|
53
|
+
// backfill-sections
|
|
300
54
|
// ---------------------------------------------------------------------------
|
|
301
55
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
56
|
+
const MemoryV3BackfillSectionsResultSchema = z.object({
|
|
57
|
+
/** Pages whose sections were embedded this pass. */
|
|
58
|
+
articles: z.number(),
|
|
59
|
+
/** Total section points upserted across all articles. */
|
|
60
|
+
sections: z.number(),
|
|
61
|
+
/** Pages whose embed threw (and was contained). */
|
|
62
|
+
failures: z.number(),
|
|
63
|
+
});
|
|
64
|
+
export type MemoryV3BackfillSectionsResult = z.infer<
|
|
65
|
+
typeof MemoryV3BackfillSectionsResultSchema
|
|
66
|
+
>;
|
|
307
67
|
|
|
308
68
|
/**
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
69
|
+
* One-time full backfill of the section dense store: embed EVERY page in the
|
|
70
|
+
* index — including synthetic skill/CLI rows the incremental maintain pass
|
|
71
|
+
* skips — into the `memory_v3_sections` collection, then advance the maintain
|
|
72
|
+
* high-water mark so the next incremental run deltas from here. Runs in-daemon
|
|
73
|
+
* so it uses the live config and so the maintain checkpoint it advances is the
|
|
74
|
+
* one the daemon's incremental pass reads.
|
|
75
|
+
*
|
|
76
|
+
* `config` is injectable for tests; production resolves the live config.
|
|
77
|
+
*/
|
|
78
|
+
export async function handleMemoryV3BackfillSections(
|
|
79
|
+
config: AssistantConfig = getConfig(),
|
|
80
|
+
): Promise<MemoryV3BackfillSectionsResult> {
|
|
81
|
+
const outcome = await backfillAllSections(config);
|
|
82
|
+
log.info(outcome, "memory-v3 section backfill complete (route)");
|
|
83
|
+
return outcome;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Route definitions (RouteHandlerArgs adapters over the handlers above)
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Mutating verbs require `settings.write`. `rebuild-index` invalidates the live
|
|
92
|
+
* lanes and `backfill-sections` writes the dense store + advances the maintain
|
|
93
|
+
* checkpoint, so a `settings.read`-only principal must not reach them.
|
|
314
94
|
*/
|
|
315
95
|
const WRITE_POLICY: RoutePolicy = {
|
|
316
96
|
requiredScopes: ["settings.write"],
|
|
@@ -319,54 +99,24 @@ const WRITE_POLICY: RoutePolicy = {
|
|
|
319
99
|
|
|
320
100
|
export const ROUTES: RouteDefinition[] = [
|
|
321
101
|
{
|
|
322
|
-
operationId: "
|
|
323
|
-
method: "POST",
|
|
324
|
-
policy: READ_POLICY,
|
|
325
|
-
endpoint: "memory/v3/health",
|
|
326
|
-
handler: () => handleMemoryV3Health(),
|
|
327
|
-
summary: "Print the v3 structural health report (read-only)",
|
|
328
|
-
tags: ["memory"],
|
|
329
|
-
responseBody: MemoryV3HealthResultSchema,
|
|
330
|
-
},
|
|
331
|
-
{
|
|
332
|
-
operationId: "memory_v3_set_core",
|
|
102
|
+
operationId: "memory_v3_rebuild_index",
|
|
333
103
|
method: "POST",
|
|
334
104
|
policy: WRITE_POLICY,
|
|
335
|
-
endpoint: "memory/v3/
|
|
336
|
-
handler:
|
|
337
|
-
|
|
338
|
-
return await handleMemoryV3SetCore(body as MemoryV3SetCoreBody);
|
|
339
|
-
} catch (err) {
|
|
340
|
-
if (err instanceof UnknownLeafError) {
|
|
341
|
-
throw new RouteError(err.message, MEMORY_V3_UNKNOWN_LEAF_CODE, 422);
|
|
342
|
-
}
|
|
343
|
-
throw err;
|
|
344
|
-
}
|
|
345
|
-
},
|
|
346
|
-
summary: "Add/remove always-on core leaves (validates + previews cost)",
|
|
105
|
+
endpoint: "memory/v3/rebuild-index",
|
|
106
|
+
handler: () => handleMemoryV3RebuildIndex(),
|
|
107
|
+
summary: "Invalidate the v3 lanes so the next turn rebuilds",
|
|
347
108
|
tags: ["memory"],
|
|
348
|
-
|
|
349
|
-
responseBody: MemoryV3SetCoreResultSchema,
|
|
109
|
+
responseBody: MemoryV3RebuildIndexResultSchema,
|
|
350
110
|
},
|
|
351
111
|
{
|
|
352
|
-
operationId: "
|
|
112
|
+
operationId: "memory_v3_backfill_sections",
|
|
353
113
|
method: "POST",
|
|
354
114
|
policy: WRITE_POLICY,
|
|
355
|
-
endpoint: "memory/v3/
|
|
356
|
-
handler: () =>
|
|
115
|
+
endpoint: "memory/v3/backfill-sections",
|
|
116
|
+
handler: () => handleMemoryV3BackfillSections(),
|
|
357
117
|
summary:
|
|
358
|
-
"
|
|
118
|
+
"One-time: embed every page's sections (incl synthetic skill/CLI rows) into the dense store",
|
|
359
119
|
tags: ["memory"],
|
|
360
|
-
responseBody:
|
|
361
|
-
},
|
|
362
|
-
{
|
|
363
|
-
operationId: "memory_v3_rebuild_index",
|
|
364
|
-
method: "POST",
|
|
365
|
-
policy: WRITE_POLICY,
|
|
366
|
-
endpoint: "memory/v3/rebuild-index",
|
|
367
|
-
handler: () => handleMemoryV3RebuildIndex(),
|
|
368
|
-
summary: "Invalidate the v3 lanes so the next turn rebuilds",
|
|
369
|
-
tags: ["memory"],
|
|
370
|
-
responseBody: MemoryV3RebuildIndexResultSchema,
|
|
120
|
+
responseBody: MemoryV3BackfillSectionsResultSchema,
|
|
371
121
|
},
|
|
372
122
|
];
|
|
@@ -18,8 +18,8 @@ mock.module("../helpers.js", () => ({
|
|
|
18
18
|
addPlaygroundMessage: async () => ({ id: "msg-test" }),
|
|
19
19
|
}));
|
|
20
20
|
|
|
21
|
-
import type { ContextWindowResult } from "../../../../context/window-manager.js";
|
|
22
21
|
import type { Conversation } from "../../../../daemon/conversation.js";
|
|
22
|
+
import type { ContextWindowResult } from "../../../../plugins/defaults/compaction/window-manager.js";
|
|
23
23
|
import type { Message } from "../../../../providers/types.js";
|
|
24
24
|
import { RouteError } from "../../errors.js";
|
|
25
25
|
import { ROUTES } from "../index.js";
|
|
@@ -17,6 +17,7 @@ import { listProviderEntries } from "../../providers/speech-to-text/provider-cat
|
|
|
17
17
|
import { resolveBatchTranscriber } from "../../providers/speech-to-text/resolve.js";
|
|
18
18
|
import { normalizeSttError } from "../../stt/daemon-batch-transcriber.js";
|
|
19
19
|
import type { BatchTranscriber, SttErrorCategory } from "../../stt/types.js";
|
|
20
|
+
import { SttError } from "../../stt/types.js";
|
|
20
21
|
import { getLogger } from "../../util/logger.js";
|
|
21
22
|
import {
|
|
22
23
|
FFMPEG_TRANSCODE_TIMEOUT_MS,
|
|
@@ -141,6 +142,27 @@ async function toWav(inputPath: string, isVideo: boolean): Promise<string> {
|
|
|
141
142
|
return wavPath;
|
|
142
143
|
}
|
|
143
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Call the provider and tag any failure as an {@link SttError} so callers can
|
|
147
|
+
* distinguish provider failures (categorized HTTP statuses) from local
|
|
148
|
+
* preprocessing failures (ffmpeg/ffprobe), which stay on the bad-gateway path.
|
|
149
|
+
*/
|
|
150
|
+
async function transcribeChunk(
|
|
151
|
+
transcriber: BatchTranscriber,
|
|
152
|
+
audioBuffer: Buffer,
|
|
153
|
+
): Promise<string> {
|
|
154
|
+
try {
|
|
155
|
+
const result = await transcriber.transcribe({
|
|
156
|
+
audio: audioBuffer,
|
|
157
|
+
mimeType: "audio/wav",
|
|
158
|
+
signal: AbortSignal.timeout(STT_REQUEST_TIMEOUT_MS),
|
|
159
|
+
});
|
|
160
|
+
return result.text;
|
|
161
|
+
} catch (err) {
|
|
162
|
+
throw normalizeSttError(err);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
144
166
|
async function transcribeWithProvider(
|
|
145
167
|
audioPath: string,
|
|
146
168
|
transcriber: BatchTranscriber,
|
|
@@ -151,12 +173,7 @@ async function transcribeWithProvider(
|
|
|
151
173
|
// If small enough, send directly
|
|
152
174
|
if (fileSize <= STT_CHUNK_MAX_BYTES) {
|
|
153
175
|
const audioBuffer = await readFile(audioPath);
|
|
154
|
-
|
|
155
|
-
audio: audioBuffer,
|
|
156
|
-
mimeType: "audio/wav",
|
|
157
|
-
signal: AbortSignal.timeout(STT_REQUEST_TIMEOUT_MS),
|
|
158
|
-
});
|
|
159
|
-
return result.text;
|
|
176
|
+
return transcribeChunk(transcriber, audioBuffer);
|
|
160
177
|
}
|
|
161
178
|
|
|
162
179
|
// Split into chunks for large files
|
|
@@ -175,12 +192,8 @@ async function transcribeWithProvider(
|
|
|
175
192
|
` Transcribing chunk ${i + 1}/${chunks.length}...\n`,
|
|
176
193
|
);
|
|
177
194
|
const audioBuffer = await readFile(chunks[i]);
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
mimeType: "audio/wav",
|
|
181
|
-
signal: AbortSignal.timeout(STT_REQUEST_TIMEOUT_MS),
|
|
182
|
-
});
|
|
183
|
-
if (result.text) parts.push(result.text);
|
|
195
|
+
const text = await transcribeChunk(transcriber, audioBuffer);
|
|
196
|
+
if (text) parts.push(text);
|
|
184
197
|
}
|
|
185
198
|
|
|
186
199
|
return parts.join(" ");
|
|
@@ -363,6 +376,26 @@ async function handleTranscribeFile({ body }: RouteHandlerArgs) {
|
|
|
363
376
|
durationSeconds,
|
|
364
377
|
};
|
|
365
378
|
} catch (err) {
|
|
379
|
+
// Only provider failures (tagged as SttError by transcribeChunk) are
|
|
380
|
+
// categorized into HTTP statuses. Local preprocessing failures (ffmpeg/
|
|
381
|
+
// ffprobe) stay on the bad-gateway path so their stderr never decides the
|
|
382
|
+
// category (e.g. a path under a dir named "403" must not become a 401).
|
|
383
|
+
if (err instanceof SttError) {
|
|
384
|
+
// Auth/rate-limit/timeout/invalid-audio are user-config or transient
|
|
385
|
+
// conditions, not daemon defects, so log them at warn to keep them out
|
|
386
|
+
// of error telemetry. Reserve error level (with the raw err for a Sentry
|
|
387
|
+
// stack) for genuine provider/unexpected failures.
|
|
388
|
+
if (err.category === "provider-error") {
|
|
389
|
+
log.error({ err, filePath }, "File transcription failed");
|
|
390
|
+
} else {
|
|
391
|
+
log.warn(
|
|
392
|
+
{ category: err.category, message: err.message, filePath },
|
|
393
|
+
"File transcription failed",
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
throw STT_ERROR_MAP[err.category]();
|
|
397
|
+
}
|
|
398
|
+
|
|
366
399
|
log.error({ err, filePath }, "File transcription failed");
|
|
367
400
|
throw new BadGatewayError(
|
|
368
401
|
err instanceof Error ? err.message : "Transcription failed",
|
|
@@ -46,14 +46,24 @@ const workspaceTreeEntrySchema = z.object({
|
|
|
46
46
|
|
|
47
47
|
type TreeEntry = z.infer<typeof workspaceTreeEntrySchema>;
|
|
48
48
|
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
|
|
49
|
+
// Recursive directory sizing walks the filesystem synchronously, so it is
|
|
50
|
+
// bounded by two caps that work together:
|
|
51
|
+
//
|
|
52
|
+
// - `DIR_SIZE_PER_DIR_ENTRY_BUDGET` caps a single directory. No one giant
|
|
53
|
+
// subtree (e.g. a `node_modules/` or `repos/` checkout) can consume the whole
|
|
54
|
+
// listing's budget and starve its siblings; only the oversized directory
|
|
55
|
+
// itself reports `size: null` (the caller renders an unknown-size
|
|
56
|
+
// placeholder) while every other folder still shows an accurate size.
|
|
57
|
+
// - `DIR_SIZE_TOTAL_ENTRY_BUDGET` caps the listing as a whole, so a workspace
|
|
58
|
+
// with many large sibling directories still can't block the daemon event loop
|
|
59
|
+
// for an unbounded amount of time. Once it is exhausted, the remaining
|
|
60
|
+
// directories report `size: null`.
|
|
61
|
+
//
|
|
62
|
+
// Each directory is allotted `min(per-dir cap, remaining listing budget)`
|
|
63
|
+
// entries, and whatever it actually consumes is deducted from the listing
|
|
64
|
+
// budget.
|
|
65
|
+
const DIR_SIZE_PER_DIR_ENTRY_BUDGET = 250_000;
|
|
66
|
+
const DIR_SIZE_TOTAL_ENTRY_BUDGET = 1_000_000;
|
|
57
67
|
|
|
58
68
|
interface DirSizeBudget {
|
|
59
69
|
remaining: number;
|
|
@@ -61,13 +71,13 @@ interface DirSizeBudget {
|
|
|
61
71
|
|
|
62
72
|
/**
|
|
63
73
|
* Recursively sum the byte size of every regular file under `absPath`,
|
|
64
|
-
*
|
|
74
|
+
* traversing up to `budget.remaining` filesystem entries.
|
|
65
75
|
*
|
|
66
76
|
* Returns:
|
|
67
77
|
* - the total size in bytes when the entire subtree was traversed within
|
|
68
78
|
* budget
|
|
69
|
-
* - `null` if the
|
|
70
|
-
*
|
|
79
|
+
* - `null` if the budget was exhausted before completion (the caller surfaces
|
|
80
|
+
* that as an unknown size in the UI)
|
|
71
81
|
*
|
|
72
82
|
* Symlinks are not followed. We rely on `withFileTypes` so we never `stat`
|
|
73
83
|
* directories purely to discover their type.
|
|
@@ -108,6 +118,27 @@ function computeDirSize(absPath: string, budget: DirSizeBudget): number | null {
|
|
|
108
118
|
return total;
|
|
109
119
|
}
|
|
110
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Compute a directory's recursive size while drawing from a shared listing
|
|
123
|
+
* budget. The directory is allotted at most `DIR_SIZE_PER_DIR_ENTRY_BUDGET`
|
|
124
|
+
* entries — and never more than the listing has left — and whatever it consumes
|
|
125
|
+
* is deducted from `listingBudget` so the next sibling sees the reduced
|
|
126
|
+
* remainder. Returns `null` (unknown size) when the allotment is exhausted.
|
|
127
|
+
*/
|
|
128
|
+
function computeDirSizeWithinListing(
|
|
129
|
+
absPath: string,
|
|
130
|
+
listingBudget: DirSizeBudget,
|
|
131
|
+
): number | null {
|
|
132
|
+
const allotment = Math.min(
|
|
133
|
+
DIR_SIZE_PER_DIR_ENTRY_BUDGET,
|
|
134
|
+
listingBudget.remaining,
|
|
135
|
+
);
|
|
136
|
+
const dirBudget: DirSizeBudget = { remaining: allotment };
|
|
137
|
+
const size = computeDirSize(absPath, dirBudget);
|
|
138
|
+
listingBudget.remaining -= allotment - dirBudget.remaining;
|
|
139
|
+
return size;
|
|
140
|
+
}
|
|
141
|
+
|
|
111
142
|
const SOUNDS_WORKSPACE_PATH = "data/sounds";
|
|
112
143
|
|
|
113
144
|
function normaliseWorkspacePathForSync(path: string): string {
|
|
@@ -153,9 +184,11 @@ function handleWorkspaceTree({ queryParams }: RouteHandlerArgs) {
|
|
|
153
184
|
const dirents = readdirSync(resolved, { withFileTypes: true });
|
|
154
185
|
const workspaceDir = getWorkspaceDir();
|
|
155
186
|
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
|
|
187
|
+
// Listing-wide budget, only constructed when sizes are requested. Each
|
|
188
|
+
// directory draws a per-directory allotment from it so one oversized
|
|
189
|
+
// subtree can't starve its siblings, while the shared remainder still caps
|
|
190
|
+
// total synchronous traversal for the whole listing.
|
|
191
|
+
const listingBudget: DirSizeBudget | undefined = includeDirSizes
|
|
159
192
|
? { remaining: DIR_SIZE_TOTAL_ENTRY_BUDGET }
|
|
160
193
|
: undefined;
|
|
161
194
|
|
|
@@ -176,7 +209,9 @@ function handleWorkspaceTree({ queryParams }: RouteHandlerArgs) {
|
|
|
176
209
|
const relativePath = fullPath.slice(workspaceDir.length + 1);
|
|
177
210
|
|
|
178
211
|
const dirSize =
|
|
179
|
-
isDir &&
|
|
212
|
+
isDir && listingBudget
|
|
213
|
+
? computeDirSizeWithinListing(fullPath, listingBudget)
|
|
214
|
+
: null;
|
|
180
215
|
|
|
181
216
|
entries.push({
|
|
182
217
|
name: entry.name,
|