@crouton-kit/crouter 0.3.37 → 0.3.39
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/dist/build-root.js +1 -0
- package/dist/{builtin-personas/runtime-base.md → builtin-memory/00-runtime-base.md} +7 -0
- package/dist/{builtin-personas/spine/has-manager.md → builtin-memory/01-spine/00-has-manager.md} +8 -0
- package/dist/builtin-memory/01-spine/01-no-manager.md +10 -0
- package/dist/{builtin-personas/lifecycle/terminal.md → builtin-memory/02-lifecycle/00-terminal.md} +8 -0
- package/dist/{builtin-personas/lifecycle/resident.md → builtin-memory/02-lifecycle/01-resident.md} +8 -0
- package/dist/{builtin-personas/waiting.md → builtin-memory/03-waiting.md} +7 -0
- package/dist/{builtin-personas/orchestration-kernel.md → builtin-memory/04-orchestration-kernel.md} +8 -0
- package/dist/{builtin-personas/advisor/PERSONA.md → builtin-memory/05-kinds/advisor/00-base.md} +5 -2
- package/dist/{builtin-personas/design/PERSONA.md → builtin-memory/05-kinds/design/00-base.md} +5 -2
- package/dist/{builtin-personas/design/orchestrator.md → builtin-memory/05-kinds/design/01-orchestrator.md} +5 -4
- package/dist/{builtin-personas/developer/PERSONA.md → builtin-memory/05-kinds/developer/00-base.md} +5 -2
- package/dist/{builtin-personas/developer/orchestrator.md → builtin-memory/05-kinds/developer/01-orchestrator.md} +5 -4
- package/dist/{builtin-personas/explore/PERSONA.md → builtin-memory/05-kinds/explore/00-base.md} +5 -2
- package/dist/{builtin-personas/explore/orchestrator.md → builtin-memory/05-kinds/explore/01-orchestrator.md} +5 -3
- package/dist/{builtin-personas/general/PERSONA.md → builtin-memory/05-kinds/general/00-base.md} +5 -2
- package/dist/{builtin-personas/general/orchestrator.md → builtin-memory/05-kinds/general/01-orchestrator.md} +5 -3
- package/dist/{builtin-personas/plan/PERSONA.md → builtin-memory/05-kinds/plan/00-base.md} +5 -2
- package/dist/{builtin-personas/plan/orchestrator.md → builtin-memory/05-kinds/plan/01-orchestrator.md} +5 -4
- package/dist/{builtin-personas/plan/reviewers/architecture-fit/PERSONA.md → builtin-memory/05-kinds/plan/reviewers/architecture-fit.md} +5 -2
- package/dist/{builtin-personas/plan/reviewers/code-smells/PERSONA.md → builtin-memory/05-kinds/plan/reviewers/code-smells.md} +5 -2
- package/dist/{builtin-personas/plan/reviewers/pattern-consistency/PERSONA.md → builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md} +5 -2
- package/dist/{builtin-personas/plan/reviewers/requirements-coverage/PERSONA.md → builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md} +5 -2
- package/dist/{builtin-personas/plan/reviewers/security/PERSONA.md → builtin-memory/05-kinds/plan/reviewers/security.md} +5 -2
- package/dist/{builtin-personas/product/PERSONA.md → builtin-memory/05-kinds/product/00-base.md} +5 -2
- package/dist/{builtin-personas/product/orchestrator.md → builtin-memory/05-kinds/product/01-orchestrator.md} +5 -4
- package/dist/{builtin-personas/product/teardown/PERSONA.md → builtin-memory/05-kinds/product/teardown.md} +5 -3
- package/dist/{builtin-personas/review/PERSONA.md → builtin-memory/05-kinds/review/00-base.md} +5 -2
- package/dist/{builtin-personas/review/orchestrator.md → builtin-memory/05-kinds/review/01-orchestrator.md} +5 -3
- package/dist/{builtin-personas/spec/PERSONA.md → builtin-memory/05-kinds/spec/00-base.md} +5 -2
- package/dist/{builtin-personas/spec/orchestrator.md → builtin-memory/05-kinds/spec/01-orchestrator.md} +5 -4
- package/dist/{builtin-personas/spec/requirements/PERSONA.md → builtin-memory/05-kinds/spec/requirements.md} +5 -2
- package/dist/builtin-memory/internal/INDEX.md +1 -1
- package/dist/builtin-memory/internal/examples/imessage-assistant.md +2 -2
- package/dist/builtin-views/prompt-review/core.mjs +6 -134
- package/dist/builtin-views/settings/core.mjs +26 -94
- package/dist/builtin-views/settings/text.mjs +0 -5
- package/dist/builtin-views/settings/tui.mjs +1 -7
- package/dist/builtin-views/settings/web.jsx +3 -49
- package/dist/clients/attach/attach-cmd.js +416 -416
- package/dist/commands/memory/read.js +3 -3
- package/dist/commands/memory/shared.d.ts +13 -8
- package/dist/commands/memory/shared.js +22 -8
- package/dist/commands/memory/write.js +5 -3
- package/dist/commands/node.js +40 -11
- package/dist/commands/profile/add-project.d.ts +1 -0
- package/dist/commands/profile/add-project.js +42 -0
- package/dist/commands/profile/delete.d.ts +1 -0
- package/dist/commands/profile/delete.js +39 -0
- package/dist/commands/profile/list.d.ts +1 -0
- package/dist/commands/profile/list.js +35 -0
- package/dist/commands/profile/new.d.ts +1 -0
- package/dist/commands/profile/new.js +50 -0
- package/dist/commands/profile/remove-project.d.ts +1 -0
- package/dist/commands/profile/remove-project.js +42 -0
- package/dist/commands/profile/rename.d.ts +1 -0
- package/dist/commands/profile/rename.js +42 -0
- package/dist/commands/profile/show.d.ts +1 -0
- package/dist/commands/profile/show.js +50 -0
- package/dist/commands/profile.d.ts +2 -0
- package/dist/commands/profile.js +33 -0
- package/dist/commands/sys/config.js +6 -21
- package/dist/commands/sys/prompt-review.js +90 -17
- package/dist/core/canvas/canvas.js +10 -9
- package/dist/core/canvas/db.js +11 -0
- package/dist/core/canvas/history.js +1 -0
- package/dist/core/canvas/types.d.ts +10 -0
- package/dist/core/config.d.ts +44 -1
- package/dist/core/config.js +155 -12
- package/dist/core/hearth/provider.d.ts +8 -0
- package/dist/core/hearth/providers/__tests__/sweep-and-release.test.d.ts +1 -0
- package/dist/core/hearth/providers/__tests__/sweep-and-release.test.js +254 -0
- package/dist/core/hearth/providers/blaxel-home.d.ts +39 -0
- package/dist/core/hearth/providers/blaxel-home.js +271 -21
- package/dist/core/hearth/providers/blaxel.d.ts +5 -0
- package/dist/core/hearth/providers/blaxel.js +86 -4
- package/dist/core/hearth/providers/types.d.ts +16 -0
- package/dist/core/memory-resolver.d.ts +26 -15
- package/dist/core/memory-resolver.js +158 -40
- package/dist/core/profiles/manifest.d.ts +33 -0
- package/dist/core/profiles/manifest.js +332 -0
- package/dist/core/profiles/select.d.ts +11 -0
- package/dist/core/profiles/select.js +115 -0
- package/dist/core/runtime/bearings.d.ts +9 -0
- package/dist/core/runtime/bearings.js +31 -0
- package/dist/core/runtime/front-door.js +9 -2
- package/dist/core/runtime/launch.d.ts +25 -7
- package/dist/core/runtime/launch.js +42 -55
- package/dist/core/runtime/nodes.d.ts +7 -0
- package/dist/core/runtime/nodes.js +6 -0
- package/dist/core/runtime/persona.js +18 -32
- package/dist/core/runtime/spawn.d.ts +13 -0
- package/dist/core/runtime/spawn.js +25 -0
- package/dist/core/scope.d.ts +14 -7
- package/dist/core/scope.js +76 -20
- package/dist/core/substrate/index.d.ts +3 -3
- package/dist/core/substrate/index.js +5 -3
- package/dist/core/substrate/on-read.js +3 -2
- package/dist/core/substrate/render.d.ts +16 -11
- package/dist/core/substrate/render.js +249 -62
- package/dist/core/substrate/schema.d.ts +11 -3
- package/dist/core/substrate/schema.js +25 -0
- package/dist/core/substrate/subject.d.ts +15 -2
- package/dist/core/substrate/subject.js +24 -2
- package/dist/hearth/control-plane/__tests__/error-serialization.test.d.ts +1 -0
- package/dist/hearth/control-plane/__tests__/error-serialization.test.js +29 -0
- package/dist/hearth/control-plane/__tests__/oauth-serving-marker.test.d.ts +1 -0
- package/dist/hearth/control-plane/__tests__/oauth-serving-marker.test.js +44 -0
- package/dist/hearth/control-plane/__tests__/wake-roll.test.d.ts +1 -0
- package/dist/hearth/control-plane/__tests__/wake-roll.test.js +230 -0
- package/dist/hearth/control-plane/db.js +27 -0
- package/dist/hearth/control-plane/hearth-target.d.ts +23 -0
- package/dist/hearth/control-plane/hearth-target.js +68 -0
- package/dist/hearth/control-plane/registry.d.ts +6 -1
- package/dist/hearth/control-plane/registry.js +7 -0
- package/dist/hearth/control-plane/relay.d.ts +4 -0
- package/dist/hearth/control-plane/relay.js +72 -3
- package/dist/hearth/control-plane/secrets.d.ts +14 -0
- package/dist/hearth/control-plane/secrets.js +21 -0
- package/dist/hearth/control-plane/server.js +140 -4
- package/dist/hearth/control-plane/serving.d.ts +15 -0
- package/dist/hearth/control-plane/serving.js +106 -0
- package/dist/hearth/control-plane/types.d.ts +20 -0
- package/dist/hearth/control-plane/wake.d.ts +8 -2
- package/dist/hearth/control-plane/wake.js +241 -3
- package/dist/pi-extensions/canvas-doc-substrate.js +2 -3
- package/dist/types.d.ts +57 -1
- package/dist/types.js +83 -1
- package/dist/web-client/assets/{index-MSKSWwV0.js → index-CbO8L0mN.js} +4 -4
- package/dist/web-client/assets/index-DwO46Cs5.css +2 -0
- package/dist/web-client/index.html +2 -2
- package/package.json +2 -2
- package/dist/builtin-memory/crouter-development/personas/base-prompt.md +0 -56
- package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +0 -57
- package/dist/builtin-memory/crouter-development/personas.md +0 -112
- package/dist/builtin-personas/spine/no-manager.md +0 -2
- package/dist/core/personas/index.d.ts +0 -13
- package/dist/core/personas/index.js +0 -11
- package/dist/core/personas/loader.d.ts +0 -159
- package/dist/core/personas/loader.js +0 -327
- package/dist/core/personas/resolve.d.ts +0 -59
- package/dist/core/personas/resolve.js +0 -376
- package/dist/web-client/assets/index-DUThOUzU.css +0 -2
|
@@ -1,10 +1,93 @@
|
|
|
1
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
1
2
|
import { general } from '../../errors.js';
|
|
2
3
|
import { nowIso } from '../../fs-utils.js';
|
|
3
4
|
import { guestEnv } from '../guest-env.js';
|
|
4
5
|
import { buildBootstrapScript } from './blaxel-bootstrap.js';
|
|
5
6
|
import { BlaxelMachineProvider } from './blaxel.js';
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
// recreate-race-fix.md §3.2 — a volume GET is one cheap API call; 1s resolution is plenty
|
|
8
|
+
// against an expected seconds-scale detach.
|
|
9
|
+
const VOLUME_DETACH_POLL_MS = 1_000;
|
|
10
|
+
// Expected wait is seconds (observed live); 120s is ~20-60x headroom over that, so this still
|
|
11
|
+
// fails deterministically inside one wake's lifetime rather than hanging forever on a genuine
|
|
12
|
+
// platform-side stuck detach.
|
|
13
|
+
const VOLUME_DETACH_TIMEOUT_MS = 120_000;
|
|
14
|
+
// orphan-cleanup-fix.md §4 — pause before the single bounded retry of a create that failed on a
|
|
15
|
+
// server-side deployment transient, to let the platform-side condition clear.
|
|
16
|
+
const CREATE_RETRY_DELAY_MS = 5_000;
|
|
17
|
+
function stableRouteName(sandboxId) {
|
|
18
|
+
return `${sandboxId}-web`;
|
|
19
|
+
}
|
|
20
|
+
/** Mint a fresh per-generation sandbox name. `providerVolumeId` is the home's stable base
|
|
21
|
+
* (never suffixed), so names stay bounded: `hearth-zt-test-1-gmchq3x2a`. Sequential recreates
|
|
22
|
+
* are seconds apart, so a ms timestamp cannot collide for one home (recreate-race-fix.md §3.2). */
|
|
23
|
+
function mintSandboxName(providerVolumeId) {
|
|
24
|
+
return `${providerVolumeId}-g${Date.now().toString(36)}`;
|
|
25
|
+
}
|
|
26
|
+
/** Escape a volumeId for safe use inside a RegExp so the generation-family anchor is exact —
|
|
27
|
+
* a home base name is user-influenced, so a literal `.` or `+` in it must not become a
|
|
28
|
+
* wildcard (orphan-cleanup-fix.md §3.1). */
|
|
29
|
+
function escapeRegExp(value) {
|
|
30
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
31
|
+
}
|
|
32
|
+
/** Predicate matching every generation of a home's name family: the stable base itself, or a
|
|
33
|
+
* `<base>-g<suffix>` generation. The `-g` anchor is load-bearing — it prevents `<base>0-g…`
|
|
34
|
+
* (e.g. `hearth-zt-test-10-g…`) from matching base `hearth-zt-test-1` (orphan-cleanup-fix.md
|
|
35
|
+
* §3.1). Suffix chars are base36 (lowercase alnum), matching `mintSandboxName`. */
|
|
36
|
+
function generationFamilyMatcher(volumeId) {
|
|
37
|
+
const gen = new RegExp(`^${escapeRegExp(volumeId)}-g[0-9a-z]+$`);
|
|
38
|
+
return (name) => name === volumeId || gen.test(name);
|
|
39
|
+
}
|
|
40
|
+
/** Serialize an arbitrary rejection to a non-empty marker string. `JSON.stringify` RETURNS
|
|
41
|
+
* `undefined` (does not throw) for `undefined`, a function, or a symbol — falling through to
|
|
42
|
+
* that would omit the `error` field from the marker's JSON line entirely, so fall back to
|
|
43
|
+
* `String(...)` whenever it yields `undefined` (matches wake.ts/relay.ts `serializeError`). */
|
|
44
|
+
function serialize(error) {
|
|
45
|
+
if (error instanceof Error)
|
|
46
|
+
return error.message;
|
|
47
|
+
try {
|
|
48
|
+
const json = JSON.stringify(error);
|
|
49
|
+
return json === undefined ? String(error) : json;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return String(error);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Emit one structured stderr line for a recreate-lifecycle marker (sweep / volume_wait /
|
|
56
|
+
* cleanup_failed / create_retry). Single writer for all four so the JSON shape stays uniform
|
|
57
|
+
* and ops log-alerting keys on `marker` (orphan-cleanup-fix.md §3.3/§3.4/§4). */
|
|
58
|
+
function emitMarker(marker, fields, level = 'info') {
|
|
59
|
+
process.stderr.write(`${JSON.stringify({ level, marker, ...fields })}\n`);
|
|
60
|
+
}
|
|
61
|
+
/** Classify a failed create+deploy as a retryable server-side deployment transient
|
|
62
|
+
* (orphan-cleanup-fix.md §4). Retryable IFF `status_code >= 500` OR `code === 'DEPLOYMENT_FAILED'`
|
|
63
|
+
* on the rejection object, with a message-substring fallback for Error-wrapped variants whose
|
|
64
|
+
* structured fields were flattened into `.message`. NEVER on 4xx — a 409 here is a real name
|
|
65
|
+
* conflict the design must surface, not absorb, so an explicit 4xx status vetoes the retry even
|
|
66
|
+
* if a `DEPLOYMENT_FAILED` code is also present. */
|
|
67
|
+
function isRetryableDeploymentError(error) {
|
|
68
|
+
if (error !== null && typeof error === 'object') {
|
|
69
|
+
// Only the canonical HTTP-status keys — NOT a bare `status`, which is an overloaded field
|
|
70
|
+
// name some envelopes use for a non-HTTP state enum/count (MINOR-2).
|
|
71
|
+
const obj = error;
|
|
72
|
+
const status = [obj.status_code, obj.statusCode].find((v) => typeof v === 'number');
|
|
73
|
+
if (typeof status === 'number') {
|
|
74
|
+
if (status >= 400 && status < 500)
|
|
75
|
+
return false; // 4xx: real conflict, never absorb
|
|
76
|
+
if (status >= 500)
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
if (obj.code === 'DEPLOYMENT_FAILED')
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
// Fallback for Error-wrapped variants: only POSITIVE server-side signals, never inferring a 4xx.
|
|
83
|
+
const message = error instanceof Error ? error.message : typeof error === 'string' ? error : '';
|
|
84
|
+
if (message !== '') {
|
|
85
|
+
if (/DEPLOYMENT_FAILED/.test(message))
|
|
86
|
+
return true;
|
|
87
|
+
if (/\bstatus[_ ]?code\b\D{0,8}5\d\d/i.test(message))
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
8
91
|
}
|
|
9
92
|
function ensureReadyHome(input) {
|
|
10
93
|
if (input.providerName !== 'blaxel') {
|
|
@@ -30,10 +113,10 @@ function isRunningMachine(machine) {
|
|
|
30
113
|
function previewEndpoint(route, fallback) {
|
|
31
114
|
return route.concreteUrl ?? route.url ?? fallback ?? null;
|
|
32
115
|
}
|
|
33
|
-
function refreshedPointers(home, provider, route, status, machine) {
|
|
116
|
+
function refreshedPointers(home, provider, route, status, sandboxId, machine) {
|
|
34
117
|
return {
|
|
35
118
|
providerName: 'blaxel',
|
|
36
|
-
providerSandboxId:
|
|
119
|
+
providerSandboxId: sandboxId,
|
|
37
120
|
providerVolumeId: home.providerVolumeId,
|
|
38
121
|
previewEndpoint: route === null ? (home.previewEndpoint ?? null) : previewEndpoint(route, home.previewEndpoint),
|
|
39
122
|
route,
|
|
@@ -80,25 +163,192 @@ export class BlaxelHomeBackend {
|
|
|
80
163
|
await this.provider.exec(current.providerSandboxId, { command: bootstrapScript, background: true, cwd: this.config.guestHome });
|
|
81
164
|
await this.provider.waitForPort(current.providerSandboxId, this.config.webPort, { maxWaitMs: 30_000 });
|
|
82
165
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
envs: guestEnv(this.config),
|
|
89
|
-
volumeMount: { volumeName: current.providerVolumeId, path: this.config.guestHome },
|
|
90
|
-
ports: [{ target: this.config.webPort, protocol: 'HTTP' }],
|
|
166
|
+
const route = await this.provider.publishHttpPort(current.providerSandboxId, {
|
|
167
|
+
machineId: current.providerSandboxId,
|
|
168
|
+
targetPort: this.config.webPort,
|
|
169
|
+
routeName: stableRouteName(current.providerSandboxId),
|
|
170
|
+
routeId: stableRouteName(current.providerSandboxId),
|
|
91
171
|
});
|
|
92
|
-
|
|
93
|
-
|
|
172
|
+
return refreshedPointers(current, this.provider, route, 'running', current.providerSandboxId, await this.getMachine(current.providerSandboxId));
|
|
173
|
+
}
|
|
174
|
+
// Terminated-machine branch (recreate-race-fix.md §3.4, orphan-cleanup-fix.md §3.3/§3.4):
|
|
175
|
+
// the crash-recovery path for a sandbox deleted recently out-of-band OR by a crashed/failed
|
|
176
|
+
// roll (a Fly redeploy restarting the CP mid-roll lands here on the next wake).
|
|
177
|
+
// `sweepAndReleaseVolume` destroys EVERY generation in this home's name family — including
|
|
178
|
+
// an unpointed leftover a crashed roll left holding the volume, which the old point-in-time
|
|
179
|
+
// attachedTo read could miss once Blaxel autonomously detached it on idle→STANDBY — then
|
|
180
|
+
// polls until the volume is confirmed free before minting a fresh name and creating.
|
|
181
|
+
await this.sweepAndReleaseVolume(current.providerVolumeId, current.homeId);
|
|
182
|
+
const { sandboxName, route, machine: created } = await this.createGeneration({
|
|
183
|
+
homeId: current.homeId,
|
|
184
|
+
tenantId: current.tenantId,
|
|
185
|
+
volumeId: current.providerVolumeId,
|
|
186
|
+
bootstrapScript,
|
|
187
|
+
});
|
|
188
|
+
return refreshedPointers(current, this.provider, route, 'running', sandboxName, created);
|
|
189
|
+
}
|
|
190
|
+
/** Destroy EVERY generation of this home via a deterministic name-family sweep — never a
|
|
191
|
+
* point-in-time `attachedTo` read, which Blaxel's autonomous idle→STANDBY detach can race
|
|
192
|
+
* ahead of, leaving a live orphan the read cannot see (orphan-cleanup-fix.md §1/§3.3). Then
|
|
193
|
+
* poll until the platform reports the volume detached. Safe because at sweep time the
|
|
194
|
+
* replacement does not exist yet (its name is minted after) and any live family member is by
|
|
195
|
+
* construction either the generation being replaced or an uncommitted leftover — the home's
|
|
196
|
+
* durable state is the volume, never a sandbox; one wake per home runs at a time. */
|
|
197
|
+
async sweepAndReleaseVolume(volumeId, homeId) {
|
|
198
|
+
// Scope the list to sandboxes Hearth tagged for THIS home (externalId = volumeId), then keep
|
|
199
|
+
// the name-family matcher as an AND-intersection on top (orphan-cleanup-fix MAJOR-1). The
|
|
200
|
+
// destroy set requires BOTH keys to agree — the tag says "Hearth created this for home X",
|
|
201
|
+
// the name says "it is in X's generation family" — so a single fault (a mistagged create, a
|
|
202
|
+
// future API silently ignoring the filter param) cannot widen the blast radius back onto a
|
|
203
|
+
// foreign home. Fail-safe defense-in-depth on a destructive op, NOT a lenient fallback.
|
|
204
|
+
const inFamily = generationFamilyMatcher(volumeId);
|
|
205
|
+
const doomed = (await this.provider.listMachines({ externalId: volumeId })).map((m) => m.machineId).filter(inFamily);
|
|
206
|
+
for (const name of doomed)
|
|
207
|
+
await this.provider.destroy(name); // sequential; destroy tolerates not_found
|
|
208
|
+
if (doomed.length > 0)
|
|
209
|
+
emitMarker('hearth.recreate.sweep', { home_id: homeId, volume_id: volumeId, destroyed: doomed });
|
|
210
|
+
await this.pollVolumeDetached(volumeId, homeId);
|
|
211
|
+
}
|
|
212
|
+
/** Poll the volume until the platform reports it detached — the create-precondition gate
|
|
213
|
+
* (recreate-race-fix.md §3.2). Reads only; never destroys. Emits `hearth.recreate.volume_wait`
|
|
214
|
+
* whenever it actually had to wait, so the re-proof can quantify headroom against the budget. */
|
|
215
|
+
async pollVolumeDetached(volumeId, homeId) {
|
|
216
|
+
const pollStartedAt = Date.now();
|
|
217
|
+
const deadline = pollStartedAt + VOLUME_DETACH_TIMEOUT_MS;
|
|
218
|
+
let waited = false;
|
|
219
|
+
for (;;) {
|
|
220
|
+
const attached = await this.provider.getVolumeAttachment(volumeId);
|
|
221
|
+
if (attached === null)
|
|
222
|
+
break;
|
|
223
|
+
waited = true;
|
|
224
|
+
if (Date.now() >= deadline) {
|
|
225
|
+
throw general(`volume ${volumeId} still attached to ${attached} after ${VOLUME_DETACH_TIMEOUT_MS}ms — cannot recreate home ${homeId}`, {
|
|
226
|
+
homeId,
|
|
227
|
+
volumeId,
|
|
228
|
+
attached,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
await delay(VOLUME_DETACH_POLL_MS);
|
|
232
|
+
}
|
|
233
|
+
if (waited) {
|
|
234
|
+
emitMarker('hearth.recreate.volume_wait', { home_id: homeId, volume_id: volumeId, waited_ms: Date.now() - pollStartedAt });
|
|
94
235
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
236
|
+
}
|
|
237
|
+
/** Create one fresh generation and bring it to a serving state (create → bootstrap exec →
|
|
238
|
+
* waitForPort → publish → getMachine), with provider self-cleanup and a single bounded
|
|
239
|
+
* retry (orphan-cleanup-fix.md §3.4/§4). Each attempt destroys the generation IT created if
|
|
240
|
+
* anything from `createMachine` through the final `getMachine` fails — no code path exits
|
|
241
|
+
* leaving a live generation it created and did not return — then rethrows the ORIGINAL error
|
|
242
|
+
* (cleanup never masks it). On the first failure, if the error is a genuine server-side
|
|
243
|
+
* deployment transient, pause, re-run ONLY the volume-free poll (the sweep already ran once),
|
|
244
|
+
* mint a NEW name, and retry exactly once; a second failure throws. The sweep is the caller's
|
|
245
|
+
* responsibility and runs before the first attempt only. */
|
|
246
|
+
async createGeneration(args) {
|
|
247
|
+
const attempt = async (sandboxName) => {
|
|
248
|
+
try {
|
|
249
|
+
await this.provider.createMachine({
|
|
250
|
+
tenantId: args.tenantId,
|
|
251
|
+
name: sandboxName,
|
|
252
|
+
// Tag EVERY generation (first-name and retry-name alike) with the home's stable volume
|
|
253
|
+
// id so the sweep's filtered list only ever sees this home's family (MAJOR-1).
|
|
254
|
+
externalId: args.volumeId,
|
|
255
|
+
...(args.image === undefined ? {} : { image: args.image }),
|
|
256
|
+
envs: guestEnv(this.config),
|
|
257
|
+
volumeMount: { volumeName: args.volumeId, path: this.config.guestHome },
|
|
258
|
+
ports: [{ target: this.config.webPort, protocol: 'HTTP' }],
|
|
259
|
+
});
|
|
260
|
+
await this.provider.exec(sandboxName, { command: args.bootstrapScript, background: true, cwd: this.config.guestHome });
|
|
261
|
+
await this.provider.waitForPort(sandboxName, this.config.webPort, { maxWaitMs: 60_000 });
|
|
262
|
+
const route = await this.provider.publishHttpPort(sandboxName, {
|
|
263
|
+
machineId: sandboxName,
|
|
264
|
+
targetPort: this.config.webPort,
|
|
265
|
+
routeName: stableRouteName(sandboxName),
|
|
266
|
+
routeId: stableRouteName(sandboxName),
|
|
267
|
+
});
|
|
268
|
+
return { route, machine: await this.getMachine(sandboxName) };
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
// §3.4 self-cleanup: destroy the generation this attempt created; never mask the original.
|
|
272
|
+
await this.provider
|
|
273
|
+
.destroy(sandboxName)
|
|
274
|
+
.catch((cleanupErr) => emitMarker('hearth.recreate.cleanup_failed', { home_id: args.homeId, sandbox: sandboxName, error: serialize(cleanupErr) }, 'warn'));
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
const firstName = mintSandboxName(args.volumeId);
|
|
279
|
+
try {
|
|
280
|
+
return { sandboxName: firstName, ...(await attempt(firstName)) };
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
if (!isRetryableDeploymentError(error))
|
|
284
|
+
throw error;
|
|
285
|
+
emitMarker('hearth.recreate.create_retry', { home_id: args.homeId, discarded_generation: firstName, error: serialize(error) }, 'warn');
|
|
286
|
+
await delay(CREATE_RETRY_DELAY_MS);
|
|
287
|
+
// Re-run ONLY the poll — NOT a re-sweep. This sits outside attempt()'s try/catch, so a
|
|
288
|
+
// poll-timeout here would otherwise mask the ORIGINAL deployment error (MINOR-3). The
|
|
289
|
+
// original is what the caller must see (it is already recorded in the create_retry
|
|
290
|
+
// marker above); on a poll failure during the retry path, rethrow it, not the timeout.
|
|
291
|
+
try {
|
|
292
|
+
await this.pollVolumeDetached(args.volumeId, args.homeId);
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
297
|
+
const secondName = mintSandboxName(args.volumeId);
|
|
298
|
+
return { sandboxName: secondName, ...(await attempt(secondName)) }; // second failure throws (self-cleaned)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
/** Targeted destroy→recreate onto a SPECIFIC image (auto-upgrade roll and
|
|
302
|
+
* rollback, design §4/§5). Unlike `wake()`'s standby-resume path, this is the ONE path
|
|
303
|
+
* that force-destroys the home's sandboxes before recreating. Cleanup is a deterministic
|
|
304
|
+
* name-family sweep (orphan-cleanup-fix.md §3.3): every generation of this home is destroyed
|
|
305
|
+
* before the fresh one is minted, so a failed roll-forward generation can never survive as a
|
|
306
|
+
* live orphan even after Blaxel autonomously detaches it from the volume. The recreate lands
|
|
307
|
+
* on a FRESH sandbox name every call — a name that has never existed cannot 409 against a
|
|
308
|
+
* lingering same-name delete. The durable volume is a separate resource and is NEVER touched
|
|
309
|
+
* (flow-D durability) — user state is byte-identical across the flip, which is exactly what
|
|
310
|
+
* makes both the upgrade and the rollback lossless. The new `providerSandboxId` is returned on
|
|
311
|
+
* the refresh; callers persist it (`refreshProviderPointers` treats it as a refreshable
|
|
312
|
+
* pointer). `templateVersion` is baked into the bootstrap's fail-fast version assert (the
|
|
313
|
+
* baked crtr must match) AND reported back so the CP records the target version — the caller
|
|
314
|
+
* still independently asserts `crtr sys version` in-guest before trusting it (wake.ts
|
|
315
|
+
* `rollHome`/`rollbackHome`, against `refresh.providerSandboxId`, never a stale captured
|
|
316
|
+
* name). */
|
|
317
|
+
async recreateOnImage(homeInput, imageRef, templateVersion) {
|
|
318
|
+
const current = ensureReadyHome(homeInput);
|
|
319
|
+
if (imageRef.trim() === '') {
|
|
320
|
+
throw general(`recreateOnImage for home ${current.homeId} requires a non-empty image ref`, { homeId: current.homeId });
|
|
321
|
+
}
|
|
322
|
+
if (templateVersion.trim() === '') {
|
|
323
|
+
throw general(`recreateOnImage for home ${current.homeId} requires a non-empty template version`, { homeId: current.homeId });
|
|
324
|
+
}
|
|
325
|
+
// Sweep the whole name family (destroys the failed roll-forward generation whether or not it
|
|
326
|
+
// still holds the volume — the exact Run-3 orphan gap) then wait until the volume is confirmed
|
|
327
|
+
// detached before minting a fresh name and creating (orphan-cleanup-fix.md §3.3).
|
|
328
|
+
await this.sweepAndReleaseVolume(current.providerVolumeId, current.homeId);
|
|
329
|
+
const bootstrapScript = buildBootstrapScript({
|
|
330
|
+
guestHome: this.config.guestHome,
|
|
331
|
+
crtrVersion: templateVersion,
|
|
332
|
+
relayToken: current.relayToken,
|
|
333
|
+
webPort: this.config.webPort,
|
|
334
|
+
nodeId: current.homeAgentTarget,
|
|
335
|
+
piAuthJson: this.config.piAuthJson,
|
|
336
|
+
piPackages: this.config.piPackages,
|
|
337
|
+
isFirstProvision: false,
|
|
338
|
+
});
|
|
339
|
+
const { sandboxName, route, machine } = await this.createGeneration({
|
|
340
|
+
homeId: current.homeId,
|
|
341
|
+
tenantId: current.tenantId,
|
|
342
|
+
volumeId: current.providerVolumeId,
|
|
343
|
+
bootstrapScript,
|
|
344
|
+
image: imageRef,
|
|
100
345
|
});
|
|
101
|
-
|
|
346
|
+
// Report the TARGET version, not the provider's construction-time value:
|
|
347
|
+
// the sandbox was recreated on `imageRef`, whose baked crtr is `templateVersion`.
|
|
348
|
+
return {
|
|
349
|
+
...refreshedPointers(current, this.provider, route, 'running', sandboxName, machine),
|
|
350
|
+
templateVersion,
|
|
351
|
+
};
|
|
102
352
|
}
|
|
103
353
|
async suspend(homeInput) {
|
|
104
354
|
const current = ensureReadyHome(homeInput);
|
|
@@ -121,7 +371,7 @@ export class BlaxelHomeBackend {
|
|
|
121
371
|
providerSandboxId: current.providerSandboxId,
|
|
122
372
|
});
|
|
123
373
|
}
|
|
124
|
-
return refreshedPointers(current, this.provider, null, machine.state, machine);
|
|
374
|
+
return refreshedPointers(current, this.provider, null, machine.state, current.providerSandboxId, machine);
|
|
125
375
|
}
|
|
126
376
|
async getMachine(machineId) {
|
|
127
377
|
const machine = await this.provider.getMachine(machineId);
|
|
@@ -7,6 +7,7 @@ interface BlaxelMachineProviderOptions {
|
|
|
7
7
|
templateVersion?: string;
|
|
8
8
|
volumeSizeGb?: number;
|
|
9
9
|
}
|
|
10
|
+
export declare function isMissingResourceError(error: unknown): boolean;
|
|
10
11
|
export declare class BlaxelMachineProvider implements MachineProvider {
|
|
11
12
|
readonly providerName: "blaxel";
|
|
12
13
|
readonly imageRef: string;
|
|
@@ -18,6 +19,9 @@ export declare class BlaxelMachineProvider implements MachineProvider {
|
|
|
18
19
|
name: string;
|
|
19
20
|
}): Promise<VolumeRef>;
|
|
20
21
|
createMachine(input: CreateMachineInput): Promise<MachineRef>;
|
|
22
|
+
listMachines(filter?: {
|
|
23
|
+
externalId?: string;
|
|
24
|
+
}): Promise<MachineRef[]>;
|
|
21
25
|
getMachine(machineId: string): Promise<MachineRef>;
|
|
22
26
|
exec(machineId: string, input: ExecInput): Promise<ExecResult>;
|
|
23
27
|
waitForPort(machineId: string, port: number, opts?: {
|
|
@@ -27,5 +31,6 @@ export declare class BlaxelMachineProvider implements MachineProvider {
|
|
|
27
31
|
health(): Promise<HealthResult>;
|
|
28
32
|
destroy(machineId: string): Promise<DestroyOutcome>;
|
|
29
33
|
destroyVolume(volumeId: string): Promise<DestroyOutcome>;
|
|
34
|
+
getVolumeAttachment(volumeId: string): Promise<string | null>;
|
|
30
35
|
}
|
|
31
36
|
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
|
-
import { SandboxInstance, VolumeInstance } from '@blaxel/core';
|
|
3
|
+
import { SandboxInstance, VolumeInstance, getVolume, listSandboxes } from '@blaxel/core';
|
|
4
4
|
import { general } from '../../errors.js';
|
|
5
5
|
const TERMINATED_STATES = new Set(['TERMINATED', 'DELETING', 'DEACTIVATED', 'DEACTIVATING', 'FAILED']);
|
|
6
6
|
const PROVISIONING_STATES = new Set(['UPLOADING', 'BUILDING', 'DEPLOYING', 'BUILT']);
|
|
@@ -81,10 +81,28 @@ function resolveExecCommand(input) {
|
|
|
81
81
|
}
|
|
82
82
|
return argv.map(shellQuote).join(' ');
|
|
83
83
|
}
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
// The Blaxel SDK (@blaxel/core 0.2.94) does not always reject with an `Error` instance — a 404
|
|
85
|
+
// comes back as a plain object (e.g. `{code:404,error:"Sandbox not found"}`), and `String()` on
|
|
86
|
+
// such an object yields the useless literal "[object Object]", which matches none of the regex
|
|
87
|
+
// below. Recognize the structured shape directly (a numeric-or-string `404` status field) AND
|
|
88
|
+
// fall back to JSON-serializing the value (mirroring wake.ts's `serializeError`) before running
|
|
89
|
+
// the message regex, so both an `Error("... not found")` and a plain-object 404 are caught.
|
|
90
|
+
export function isMissingResourceError(error) {
|
|
91
|
+
const fields = record(error);
|
|
92
|
+
if (fields !== null) {
|
|
93
|
+
for (const key of ['code', 'status', 'status_code', 'statusCode']) {
|
|
94
|
+
const value = fields[key];
|
|
95
|
+
if (value === 404 || value === '404')
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const message = error instanceof Error ? error.message : (JSON.stringify(error) ?? String(error));
|
|
86
100
|
return /\b(not found|404|does not exist|missing)\b/i.test(message);
|
|
87
101
|
}
|
|
102
|
+
// Accepts either a live `SandboxInstance` or a raw wire `Sandbox` page item — only the
|
|
103
|
+
// `metadata`/`status`/`spec` fields are read, so mapping raw list pages avoids constructing
|
|
104
|
+
// `SandboxInstance`s (whose `list()` eagerly warms an H2 session per sandbox — pointless for a
|
|
105
|
+
// destroy sweep).
|
|
88
106
|
function toMachineRef(sandbox) {
|
|
89
107
|
const metadata = sandbox.metadata;
|
|
90
108
|
const status = sandbox.status ?? undefined;
|
|
@@ -142,7 +160,16 @@ export class BlaxelMachineProvider {
|
|
|
142
160
|
async createMachine(input) {
|
|
143
161
|
const sandbox = await SandboxInstance.createIfNotExists({
|
|
144
162
|
name: input.name ?? input.tenantId,
|
|
145
|
-
|
|
163
|
+
// Tag every sandbox with the home's stable volume id so the recreate sweep can scope its
|
|
164
|
+
// destructive list to this home's family only (orphan-cleanup-fix MAJOR-1). Unset on
|
|
165
|
+
// callers that pass no externalId.
|
|
166
|
+
...(input.externalId === undefined ? {} : { externalId: input.externalId }),
|
|
167
|
+
// A targeted recreate (auto-upgrade roll / rollback, design §4.1) passes
|
|
168
|
+
// an explicit `image`; the normal provision/wake path leaves it unset
|
|
169
|
+
// and uses the provider's construction-time config image. `createIfNotExists`
|
|
170
|
+
// only honors this on genuine creation — the roll path destroys the old
|
|
171
|
+
// sandbox first so the name is free (blaxel-home.ts `recreateOnImage`).
|
|
172
|
+
image: input.image ?? this.config.image,
|
|
146
173
|
memory: this.config.memory,
|
|
147
174
|
region: this.config.region,
|
|
148
175
|
volumes: input.volumeMount === undefined || input.volumeMount === null ? undefined : [{ name: input.volumeMount.volumeName, mountPath: input.volumeMount.path, readOnly: false }],
|
|
@@ -158,6 +185,38 @@ export class BlaxelMachineProvider {
|
|
|
158
185
|
await sandbox.wait({ maxWait: 90_000, interval: 500 }).catch(() => sandbox);
|
|
159
186
|
return toMachineRef(sandbox);
|
|
160
187
|
}
|
|
188
|
+
// orphan-cleanup-fix.md §3.2 + MAJOR-1/MINOR-1: the deterministic name-family sweep enumerates
|
|
189
|
+
// a home's generations via LIST scoped to the caller-owned `externalId` tag, then destroys the
|
|
190
|
+
// ones whose name is in the family. Over-listing is harmless — a FAILED/detached record still
|
|
191
|
+
// shows up here and destroy tolerates not_found, which is exactly what closes the autonomous-
|
|
192
|
+
// standby-detach orphan gap. Drives the generated `listSandboxes` client directly (bypassing
|
|
193
|
+
// `SandboxInstance.list()`, which never follows a cursor and warms an H2 session per sandbox):
|
|
194
|
+
// loops until exhausted so a future SDK version crossing Blaxel-Version 2026-04-28 (cursor
|
|
195
|
+
// pagination, 50/page) cannot silently truncate the sweep to page 1. At the SDK's pinned
|
|
196
|
+
// 2026-04-16 the server returns a bare array (the full set) and the loop breaks on the first
|
|
197
|
+
// page.
|
|
198
|
+
async listMachines(filter) {
|
|
199
|
+
const machines = [];
|
|
200
|
+
let cursor;
|
|
201
|
+
for (;;) {
|
|
202
|
+
const { data: raw } = await listSandboxes({
|
|
203
|
+
query: {
|
|
204
|
+
limit: 200,
|
|
205
|
+
...(cursor === undefined ? {} : { cursor }),
|
|
206
|
+
...(filter?.externalId === undefined ? {} : { externalId: filter.externalId }),
|
|
207
|
+
},
|
|
208
|
+
throwOnError: true,
|
|
209
|
+
});
|
|
210
|
+
const page = Array.isArray(raw) ? raw : (raw?.data ?? []);
|
|
211
|
+
machines.push(...page.map(toMachineRef));
|
|
212
|
+
if (Array.isArray(raw))
|
|
213
|
+
break; // legacy bare array (Blaxel-Version < 2026-04-28) = full set
|
|
214
|
+
cursor = raw?.meta?.nextCursor || undefined; // empty when there are no more pages
|
|
215
|
+
if (cursor === undefined)
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
return machines;
|
|
219
|
+
}
|
|
161
220
|
async getMachine(machineId) {
|
|
162
221
|
try {
|
|
163
222
|
const sandbox = await SandboxInstance.get(machineId);
|
|
@@ -279,4 +338,27 @@ export class BlaxelMachineProvider {
|
|
|
279
338
|
throw error;
|
|
280
339
|
}
|
|
281
340
|
}
|
|
341
|
+
// recreate-race-fix.md §3.1: read the platform's own positive attachment signal
|
|
342
|
+
// (`Volume.state.attachedTo`) rather than inferring detach from a sandbox GET/delete —
|
|
343
|
+
// that read/create consistency split is exactly what raced the old same-name recreate.
|
|
344
|
+
// `VolumeInstance.get`'s backing `volume` field is TS-private (no public state getter
|
|
345
|
+
// in @blaxel/core 0.2.94), so this calls the underlying `getVolume` SDK function
|
|
346
|
+
// directly instead — same request `VolumeInstance.get` makes, with `state` exposed.
|
|
347
|
+
// A 404 (or any other failure) throws, wrapped with the volumeId for context: a missing
|
|
348
|
+
// home volume is a real incident, never something to continue past (volume-is-sacred).
|
|
349
|
+
async getVolumeAttachment(volumeId) {
|
|
350
|
+
let attachedTo;
|
|
351
|
+
try {
|
|
352
|
+
const { data } = await getVolume({ path: { volumeName: volumeId }, throwOnError: true });
|
|
353
|
+
attachedTo = data.state?.attachedTo;
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
throw general(`blaxel volume ${volumeId} attachment check failed`, {
|
|
357
|
+
volumeId,
|
|
358
|
+
cause: error instanceof Error ? error.message : String(error),
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
const trimmed = (attachedTo ?? '').trim();
|
|
362
|
+
return trimmed === '' ? null : trimmed;
|
|
363
|
+
}
|
|
282
364
|
}
|
|
@@ -22,10 +22,12 @@ export interface VolumeRef {
|
|
|
22
22
|
}
|
|
23
23
|
export interface CreateMachineInput {
|
|
24
24
|
tenantId: string;
|
|
25
|
+
externalId?: string;
|
|
25
26
|
template?: string | null;
|
|
26
27
|
envs?: Record<string, string>;
|
|
27
28
|
enableNetwork?: boolean;
|
|
28
29
|
name?: string | null;
|
|
30
|
+
image?: string | null;
|
|
29
31
|
ports?: {
|
|
30
32
|
target: number;
|
|
31
33
|
protocol: 'HTTP';
|
|
@@ -65,6 +67,16 @@ export interface MachineProvider {
|
|
|
65
67
|
}): Promise<VolumeRef>;
|
|
66
68
|
createMachine(input: CreateMachineInput): Promise<MachineRef>;
|
|
67
69
|
getMachine(machineId: MachineId): Promise<MachineRef>;
|
|
70
|
+
/** List machine records, optionally scoped to a caller-owned `externalId` tag. The recreate
|
|
71
|
+
* cleanup sweep passes the home's `providerVolumeId` as the filter so only sandboxes Hearth
|
|
72
|
+
* itself tagged for this home come back, then intersects that with the generation NAME FAMILY
|
|
73
|
+
* matcher — a tag∩name intersection on a destructive op, so no id-format or workspace name
|
|
74
|
+
* discipline is load-bearing for other homes' safety (orphan-cleanup-fix MAJOR-1). The result
|
|
75
|
+
* is NOT a promise of every machine in the workspace; without a filter it lists broadly, with
|
|
76
|
+
* one it lists only tagged matches. Implementations must page until exhausted (MINOR-1). */
|
|
77
|
+
listMachines(filter?: {
|
|
78
|
+
externalId?: string;
|
|
79
|
+
}): Promise<MachineRef[]>;
|
|
68
80
|
exec(machineId: MachineId, input: ExecInput): Promise<ExecResult>;
|
|
69
81
|
waitForPort(machineId: MachineId, port: number, opts?: {
|
|
70
82
|
maxWaitMs?: number;
|
|
@@ -73,5 +85,9 @@ export interface MachineProvider {
|
|
|
73
85
|
health(): Promise<HealthResult>;
|
|
74
86
|
destroy(machineId: MachineId): Promise<DestroyOutcome>;
|
|
75
87
|
destroyVolume(volumeId: string): Promise<DestroyOutcome>;
|
|
88
|
+
/** The resource currently holding this volume ("sandbox:<name>"), or null if detached.
|
|
89
|
+
* A missing volume (404) throws — a home volume disappearing is always an incident,
|
|
90
|
+
* never something to continue past (recreate-race-fix.md §3.1). */
|
|
91
|
+
getVolumeAttachment(volumeId: string): Promise<string | null>;
|
|
76
92
|
}
|
|
77
93
|
export type { MachineState, M0Config, RouteRef };
|
|
@@ -2,18 +2,20 @@ import type { InstalledPlugin, Scope } from '../types.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Thin memory-document resolver for the document substrate. Resolution is scope
|
|
4
4
|
* + leaf/path only: qualifier parse → scope precedence → direct path lookup →
|
|
5
|
-
* leaf-name fallback. The
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* leaf-name fallback. The memory scopes resolve in precedence order project
|
|
6
|
+
* stack > profile > user > builtin. It returns the raw parsed frontmatter +
|
|
7
|
+
* body; it does NOT interpret the schema, kind, gate, or rungs — that is the
|
|
8
|
+
* schema/gate layer's job (callers filter by `frontmatter.kind`). Project
|
|
9
|
+
* resolution is a nearest-first stack of every ancestor `.crouter/` (widened by
|
|
10
|
+
* a selected profile's `projects` manifest entries — see `findProjectScopeRoots`
|
|
11
|
+
* in scope.ts), profile is the selected profile's own singleton `memory/` store
|
|
12
|
+
* (from `CRTR_PROFILE_ID`), and user/builtin remain singleton scopes.
|
|
11
13
|
*/
|
|
12
14
|
export interface MemoryDoc {
|
|
13
15
|
/** Path-derived identity: the doc's path under the scope's memory/ root, no
|
|
14
16
|
* extension, slash-separated — e.g. memory/taste/foo.md → "taste/foo". */
|
|
15
17
|
name: string;
|
|
16
|
-
scope:
|
|
18
|
+
scope: MemoryScope;
|
|
17
19
|
/** Absolute path to the resolved .md file. */
|
|
18
20
|
path: string;
|
|
19
21
|
/** Raw, uncoerced frontmatter record (null when the doc has no frontmatter). */
|
|
@@ -21,26 +23,35 @@ export interface MemoryDoc {
|
|
|
21
23
|
/** Document body, with the frontmatter block stripped. */
|
|
22
24
|
body: string;
|
|
23
25
|
}
|
|
26
|
+
/** The memory-only scope union: the global `Scope` (`user|project|builtin`)
|
|
27
|
+
* plus `profile`. Confined to the memory resolver/list/read/write/render
|
|
28
|
+
* paths — deliberately NOT folded into the global `Scope` union, because that
|
|
29
|
+
* would leak `profile` into config, plugin, marketplace, view, and doctor/
|
|
30
|
+
* sys-config surfaces the spec does not extend. */
|
|
31
|
+
export type MemoryScope = Scope | 'profile';
|
|
24
32
|
export interface MemoryResolutionOpts {
|
|
25
33
|
/** Restrict resolution to a single scope. Conflicts with a scope prefix on
|
|
26
34
|
* the identifier (e.g. `user/foo` with scope=project) throw. */
|
|
27
|
-
scope?:
|
|
35
|
+
scope?: MemoryScope;
|
|
28
36
|
}
|
|
29
37
|
/** Canonical, unambiguous identifier for a memory document: `<scope>/<name>`. */
|
|
30
38
|
export declare function memoryDocId(doc: MemoryDoc): string;
|
|
31
39
|
/** All native memory docs for a scope. Project scope is a nearest-first stack of
|
|
32
|
-
* every ancestor `.crouter/memory
|
|
33
|
-
|
|
40
|
+
* every ancestor `.crouter/memory/` (widened by a selected profile's project
|
|
41
|
+
* stack); profile is the selected profile's own singleton store, resolved
|
|
42
|
+
* through `loadProfileManifest`; user and builtin are singleton stores. */
|
|
43
|
+
export declare function listMemoryDocs(scope: MemoryScope, quiet?: boolean): MemoryDoc[];
|
|
34
44
|
/** All of one plugin's substrate docs, mounted under the virtual `<pluginName>/`
|
|
35
45
|
* namespace. Walks `pluginMemoryDir(plugin)` recursively for *.md, deriving each
|
|
36
46
|
* doc's name exactly as `listMemoryDocs` does (path-relative, no extension,
|
|
37
47
|
* slash-separated) then prefixing the plugin name. Builtin has no plugins. */
|
|
38
|
-
export declare function listPluginMemoryDocs(plugin: InstalledPlugin, scope:
|
|
48
|
+
export declare function listPluginMemoryDocs(plugin: InstalledPlugin, scope: MemoryScope, quiet?: boolean): MemoryDoc[];
|
|
39
49
|
/** All memory docs across the resolved sources, in precedence order: each
|
|
40
|
-
* ancestor project `.crouter/` from nearest to farthest, then
|
|
41
|
-
*
|
|
42
|
-
* docs, so native wins on the
|
|
43
|
-
|
|
50
|
+
* ancestor project `.crouter/` from nearest to farthest, then the selected
|
|
51
|
+
* profile's memory (if any), then user, then builtin. Within each source,
|
|
52
|
+
* native docs are emitted before enabled-plugin docs, so native wins on the
|
|
53
|
+
* caller's first-wins dedup. */
|
|
54
|
+
export declare function listAllMemoryDocs(scope?: MemoryScope, quiet?: boolean): MemoryDoc[];
|
|
44
55
|
/**
|
|
45
56
|
* Resolve a path-derived name to a single memory document.
|
|
46
57
|
*
|