sealkeep 0.9.0 → 0.11.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 +163 -14
- package/CHANGELOG.md +223 -1
- package/CONTROL_PLANE.md +2 -2
- package/LICENSE +1 -1
- package/README.md +108 -22
- package/THIRD_PARTY.md +2 -2
- package/THREAT_MODEL.md +23 -4
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/chunk-access.d.ts +26 -4
- package/dist/packages/sealkeep-crypto/src/chunk-access.js +219 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.d.ts +11 -1
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.js +58 -21
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.d.ts +2 -2
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.d.ts +1 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.js +1 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.js +4 -4
- package/dist/packages/sealkeep-crypto/src/squeeze.d.ts +6 -0
- package/dist/packages/sealkeep-crypto/src/squeeze.js +39 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.d.ts +36 -3
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.js +196 -37
- package/dist/site/index.html +1808 -1904
- package/dist/site/llms.txt +67 -0
- package/dist/site/trust/architecture-data-flow.html +53 -0
- package/dist/site/trust/audit-roadmap.html +37 -0
- package/dist/site/trust/deployment-responsibility.html +11 -0
- package/dist/site/trust/dpa-sample.html +30 -0
- package/dist/site/trust/release-provenance.html +21 -0
- package/dist/site/trust/subprocessors.html +15 -0
- package/dist/site/trust/threat-model.html +34 -0
- package/dist/site/trust/toms.html +41 -0
- package/dist/site/trust-document.css +32 -0
- package/dist/site/trust.html +73 -0
- package/dist/site/visual/assets/index-8Nxnkc7o.js +207 -0
- package/dist/site/visual/assets/index-oxLy1bpy.css +1 -0
- package/dist/site/visual/index.html +18 -0
- package/dist/site.zip +0 -0
- package/dist/src/activity.d.ts +9 -0
- package/dist/src/activity.js +90 -1
- package/dist/src/adapters.d.ts +175 -5
- package/dist/src/adapters.js +961 -73
- package/dist/src/agent-context.d.ts +135 -0
- package/dist/src/agent-context.js +1059 -0
- package/dist/src/archive-copies.d.ts +47 -0
- package/dist/src/archive-copies.js +179 -0
- package/dist/src/audit.d.ts +1 -1
- package/dist/src/audit.js +29 -4
- package/dist/src/autopilot.d.ts +52 -7
- package/dist/src/autopilot.js +143 -25
- package/dist/src/background-bandwidth.d.ts +46 -0
- package/dist/src/background-bandwidth.js +301 -0
- package/dist/src/background-cpu.d.ts +82 -0
- package/dist/src/background-cpu.js +212 -0
- package/dist/src/background-worker-error.d.ts +12 -0
- package/dist/src/background-worker-error.js +18 -0
- package/dist/src/branding.d.ts +24 -1
- package/dist/src/branding.js +31 -1
- package/dist/src/bridge.d.ts +233 -0
- package/dist/src/bridge.js +604 -0
- package/dist/src/byte-stream.d.ts +91 -0
- package/dist/src/byte-stream.js +385 -0
- package/dist/src/chunk-store.d.ts +41 -8
- package/dist/src/chunk-store.js +161 -65
- package/dist/src/cli.js +1698 -163
- package/dist/src/cloud.d.ts +841 -31
- package/dist/src/cloud.js +3196 -277
- package/dist/src/context-background.d.ts +37 -0
- package/dist/src/context-background.js +309 -0
- package/dist/src/context-drain-child.d.ts +1 -0
- package/dist/src/context-drain-child.js +98 -0
- package/dist/src/context-reader.d.ts +118 -0
- package/dist/src/context-reader.js +447 -0
- package/dist/src/control-plane/auth.d.ts +32 -4
- package/dist/src/control-plane/auth.js +85 -24
- package/dist/src/control-plane/server.js +19 -6
- package/dist/src/control-plane.d.ts +17 -1
- package/dist/src/control-plane.js +32 -6
- package/dist/src/crypto.d.ts +1 -1
- package/dist/src/crypto.js +5 -5
- package/dist/src/daemon-lease.d.ts +70 -0
- package/dist/src/daemon-lease.js +420 -0
- package/dist/src/daemon.d.ts +94 -1
- package/dist/src/daemon.js +1082 -105
- package/dist/src/darwin-service-policy.d.ts +41 -0
- package/dist/src/darwin-service-policy.js +60 -0
- package/dist/src/dashboard-cli.js +15 -15
- package/dist/src/device-authorization.d.ts +37 -0
- package/dist/src/device-authorization.js +199 -0
- package/dist/src/device-enrollment.d.ts +91 -0
- package/dist/src/device-enrollment.js +349 -0
- package/dist/src/disk.d.ts +17 -12
- package/dist/src/disk.js +43 -17
- package/dist/src/doctor.d.ts +35 -1
- package/dist/src/doctor.js +316 -41
- package/dist/src/durable-ticket-lock.d.ts +24 -0
- package/dist/src/durable-ticket-lock.js +232 -0
- package/dist/src/enroll.d.ts +1 -1
- package/dist/src/enroll.js +13 -7
- package/dist/src/env.d.ts +10 -1
- package/dist/src/env.js +11 -3
- package/dist/src/errors.d.ts +8 -8
- package/dist/src/errors.js +6 -6
- package/dist/src/flush.d.ts +12 -0
- package/dist/src/flush.js +37 -0
- package/dist/src/heartbeat.d.ts +86 -12
- package/dist/src/heartbeat.js +415 -29
- package/dist/src/index-background-watchdog.d.ts +1 -0
- package/dist/src/index-background-watchdog.js +94 -0
- package/dist/src/index-background-work.d.ts +21 -0
- package/dist/src/index-background-work.js +25 -0
- package/dist/src/index-background.d.ts +64 -0
- package/dist/src/index-background.js +394 -0
- package/dist/src/index-build-child.d.ts +1 -0
- package/dist/src/index-build-child.js +109 -0
- package/dist/src/index-manifest.d.ts +52 -0
- package/dist/src/index-manifest.js +444 -0
- package/dist/src/index-publication-proof.d.ts +84 -0
- package/dist/src/index-publication-proof.js +380 -0
- package/dist/src/index-publication-state.d.ts +149 -0
- package/dist/src/index-publication-state.js +696 -0
- package/dist/src/index-publication-verifier.d.ts +89 -0
- package/dist/src/index-publication-verifier.js +341 -0
- package/dist/src/index-publish.d.ts +62 -0
- package/dist/src/index-publish.js +540 -0
- package/dist/src/index-scratch-cleanup.d.ts +19 -0
- package/dist/src/index-scratch-cleanup.js +166 -0
- package/dist/src/index-segment-types.d.ts +132 -0
- package/dist/src/index-segment-types.js +21 -0
- package/dist/src/index-segments.d.ts +9 -0
- package/dist/src/index-segments.js +516 -0
- package/dist/src/index-store.d.ts +123 -0
- package/dist/src/index-store.js +495 -0
- package/dist/src/index-sync.d.ts +91 -15
- package/dist/src/index-sync.js +286 -53
- package/dist/src/index-upgrade-publication.d.ts +30 -0
- package/dist/src/index-upgrade-publication.js +179 -0
- package/dist/src/integration-manager.d.ts +32 -0
- package/dist/src/integration-manager.js +394 -0
- package/dist/src/leakscan.js +1 -1
- package/dist/src/local-api.d.ts +56 -15
- package/dist/src/local-api.js +4979 -541
- package/dist/src/machine-settings.d.ts +51 -0
- package/dist/src/machine-settings.js +166 -0
- package/dist/src/managed-chunks.d.ts +5 -2
- package/dist/src/managed-chunks.js +14 -14
- package/dist/src/mcp-install.d.ts +11 -9
- package/dist/src/mcp-install.js +73 -28
- package/dist/src/mcp-workspace.d.ts +18 -0
- package/dist/src/mcp-workspace.js +50 -0
- package/dist/src/mcp.js +294 -25
- package/dist/src/migrate.js +27 -21
- package/dist/src/notify.d.ts +1 -1
- package/dist/src/notify.js +5 -5
- package/dist/src/offload.d.ts +201 -14
- package/dist/src/offload.js +1848 -140
- package/dist/src/onboarding.d.ts +8 -1
- package/dist/src/onboarding.js +4 -4
- package/dist/src/packages.d.ts +2 -2
- package/dist/src/packages.js +10 -2
- package/dist/src/passkey.d.ts +0 -1
- package/dist/src/passkey.js +2 -7
- package/dist/src/password-lock.d.ts +2 -2
- package/dist/src/password-lock.js +6 -6
- package/dist/src/paths.d.ts +2 -0
- package/dist/src/paths.js +2 -0
- package/dist/src/presence.d.ts +86 -0
- package/dist/src/presence.js +240 -0
- package/dist/src/progress-deadline.d.ts +21 -0
- package/dist/src/progress-deadline.js +91 -0
- package/dist/src/project-repair.d.ts +55 -0
- package/dist/src/project-repair.js +131 -0
- package/dist/src/providers/gcs.d.ts +28 -7
- package/dist/src/providers/gcs.js +35 -24
- package/dist/src/providers/gdrive.d.ts +71 -8
- package/dist/src/providers/gdrive.js +223 -50
- package/dist/src/providers/index.d.ts +11 -3
- package/dist/src/providers/index.js +30 -10
- package/dist/src/providers/s3.d.ts +30 -8
- package/dist/src/providers/s3.js +41 -30
- package/dist/src/providers/safe-storage-fetch.d.ts +12 -0
- package/dist/src/providers/safe-storage-fetch.js +72 -0
- package/dist/src/queue.d.ts +136 -19
- package/dist/src/queue.js +862 -96
- package/dist/src/reclaim-transaction.d.ts +156 -0
- package/dist/src/reclaim-transaction.js +1027 -0
- package/dist/src/recovery-codes.d.ts +32 -0
- package/dist/src/recovery-codes.js +338 -0
- package/dist/src/recovery.js +12 -9
- package/dist/src/rehydrate.d.ts +25 -22
- package/dist/src/rehydrate.js +319 -23
- package/dist/src/restore.d.ts +57 -4
- package/dist/src/restore.js +272 -36
- package/dist/src/resume-stub.d.ts +92 -0
- package/dist/src/resume-stub.js +417 -0
- package/dist/src/retention.d.ts +98 -7
- package/dist/src/retention.js +1066 -62
- package/dist/src/rotate.js +3 -3
- package/dist/src/search.d.ts +566 -8
- package/dist/src/search.js +5897 -290
- package/dist/src/secrets.d.ts +51 -7
- package/dist/src/secrets.js +316 -24
- package/dist/src/service.d.ts +49 -11
- package/dist/src/service.js +776 -35
- package/dist/src/share.js +3 -3
- package/dist/src/shared-spaces.d.ts +98 -0
- package/dist/src/shared-spaces.js +214 -0
- package/dist/src/source-reader.d.ts +73 -0
- package/dist/src/source-reader.js +715 -0
- package/dist/src/spool.d.ts +1 -1
- package/dist/src/spool.js +1 -1
- package/dist/src/start-tui.js +2 -1
- package/dist/src/start.js +2 -2
- package/dist/src/storage-endpoint.d.ts +21 -0
- package/dist/src/storage-endpoint.js +122 -0
- package/dist/src/storage-setup.js +12 -12
- package/dist/src/storage-targets.d.ts +109 -6
- package/dist/src/storage-targets.js +975 -67
- package/dist/src/stream-to-cloud.d.ts +5 -1
- package/dist/src/stream-to-cloud.js +34 -14
- package/dist/src/sync-rules.d.ts +31 -6
- package/dist/src/sync-rules.js +153 -14
- package/dist/src/team-backfill-scheduling.d.ts +8 -0
- package/dist/src/team-backfill-scheduling.js +33 -0
- package/dist/src/team-backfill.d.ts +116 -0
- package/dist/src/team-backfill.js +1429 -0
- package/dist/src/team-index-cache.d.ts +16 -0
- package/dist/src/team-index-cache.js +152 -0
- package/dist/src/team-offboarding.d.ts +38 -0
- package/dist/src/team-offboarding.js +1043 -0
- package/dist/src/team-presence.d.ts +127 -0
- package/dist/src/team-presence.js +904 -0
- package/dist/src/team-publication-policy.d.ts +20 -0
- package/dist/src/team-publication-policy.js +140 -0
- package/dist/src/team-realtime.d.ts +68 -0
- package/dist/src/team-realtime.js +816 -0
- package/dist/src/team-source-facts-cache.d.ts +23 -0
- package/dist/src/team-source-facts-cache.js +255 -0
- package/dist/src/trash.d.ts +1 -1
- package/dist/src/trash.js +2 -2
- package/dist/src/tui.js +11 -12
- package/dist/src/types.d.ts +173 -7
- package/dist/src/types.js +20 -0
- package/dist/src/ui-server.d.ts +163 -35
- package/dist/src/ui-server.js +712 -72
- package/dist/src/ui.d.ts +1 -2
- package/dist/src/ui.js +1 -2
- package/dist/src/upload.d.ts +27 -0
- package/dist/src/upload.js +383 -43
- package/dist/src/vault.d.ts +226 -30
- package/dist/src/vault.js +1776 -192
- package/dist/src/watcher.d.ts +7 -1
- package/dist/src/watcher.js +198 -55
- package/dist/src/worker.d.ts +27 -3
- package/dist/src/worker.js +274 -55
- package/package.json +33 -12
- package/scripts/native-reboot-rehearsal.mjs +90 -0
- package/web/app.js +6032 -343
- package/web/bootstrap.js +17 -0
- package/web/index.html +255 -57
- package/web/rail.js +317 -40
- package/web/retention.html +2 -2
- package/web/rules-view.js +188 -16
- package/web/sessions-view.js +485 -62
- package/web/sessions.html +2 -2
- package/web/setup-api.js +152 -29
- package/web/setup-logic.js +68 -9
- package/web/setup.html +113 -44
- package/web/setup.js +604 -71
- package/web/style.css +513 -98
- package/dist/packages/vaultline-crypto/src/chunk-access.js +0 -93
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.d.ts +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.js +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.js +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.d.ts +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.d.ts +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.js +0 -0
package/dist/src/mcp.js
CHANGED
|
@@ -1,60 +1,329 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
-
import { archiveFile, defaultDataDir, listArchives, previewRetention, vaultStatus } from "./vault.js";
|
|
4
|
+
import { archiveFile, defaultDataDir, listArchives, previewRetention, readConfig, vaultStatus } from "./vault.js";
|
|
5
5
|
import { restoreArchive } from "./restore.js";
|
|
6
6
|
import { providers } from "./control-plane.js";
|
|
7
7
|
import { envVar } from "./env.js";
|
|
8
|
+
import { search } from "./search.js";
|
|
9
|
+
import { watchOnce, publishOnce, defaultMirrorPath, publishBridgeMessage, pullBridgeMessages, drainPending, promptInjection } from "./bridge.js";
|
|
10
|
+
import { readFile } from "node:fs/promises";
|
|
11
|
+
import { readContext } from "./context-reader.js";
|
|
12
|
+
import { resolveMcpWorkspace } from "./mcp-workspace.js";
|
|
13
|
+
const sourceSchema = z.object({
|
|
14
|
+
version: z.literal(1), archiveId: z.string().uuid(), envelopeArchiveId: z.string().uuid(),
|
|
15
|
+
sourceSha256: z.string().regex(/^[a-f0-9]{64}$/i), sourceBytes: z.number().int().nonnegative(),
|
|
16
|
+
ciphertextSha256: z.string().regex(/^[a-f0-9]{64}$/i), project: z.string().optional(),
|
|
17
|
+
projectKey: z.string().optional(), spaceKey: z.string().optional(),
|
|
18
|
+
});
|
|
8
19
|
const dataDir = envVar("DATA_DIR") ?? defaultDataDir();
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
20
|
+
/**
|
|
21
|
+
* The phrase, from the env var or from this machine's keystore.
|
|
22
|
+
*
|
|
23
|
+
* Reading ONLY the env var meant an agent's search was metadata-only forever
|
|
24
|
+
* unless the person pasted their recovery phrase in plaintext into the
|
|
25
|
+
* agent's MCP config — the exact place a phrase must not live. The CLI's own
|
|
26
|
+
* commands already resolve env-then-keystore; the MCP now resolves the same
|
|
27
|
+
* way, so a vault that "seals on its own" also searches on its own.
|
|
28
|
+
*/
|
|
29
|
+
let phrasePromise = null;
|
|
30
|
+
function phraseAvailable() {
|
|
31
|
+
phrasePromise ??= (async () => {
|
|
32
|
+
const fromEnv = envVar("RECOVERY_PHRASE");
|
|
33
|
+
if (fromEnv)
|
|
34
|
+
return fromEnv;
|
|
35
|
+
try {
|
|
36
|
+
const { readConfig } = await import("./vault.js");
|
|
37
|
+
const { resolveRecoveryPhrase } = await import("./secrets.js");
|
|
38
|
+
const { vaultId } = await readConfig(dataDir);
|
|
39
|
+
return (await resolveRecoveryPhrase(dataDir, vaultId)) ?? undefined;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
})();
|
|
45
|
+
return phrasePromise;
|
|
46
|
+
}
|
|
47
|
+
const server = new McpServer({ name: "sealkeep", version: "0.1.0" });
|
|
48
|
+
function text(value, compact = false) { return { content: [{ type: "text", text: JSON.stringify(value, null, compact ? undefined : 2) }] }; }
|
|
49
|
+
const PROJECT_KEY_RE = /^(?:g1|p1):[a-f0-9]{64}$/;
|
|
50
|
+
/** A confirmed checkout can name its team before it has any local archives.
|
|
51
|
+
* Labels remain hints, never a reason to union distinct stable identities. */
|
|
52
|
+
async function resolveProject(project, projectKey) {
|
|
53
|
+
const records = project ? await listArchives(dataDir) : [];
|
|
54
|
+
if (projectKey && !project) {
|
|
55
|
+
return { records, note: "projectKey must be paired with its display project" };
|
|
56
|
+
}
|
|
57
|
+
if (!project || projectKey)
|
|
58
|
+
return { records, projectKey };
|
|
59
|
+
const config = await readConfig(dataDir);
|
|
60
|
+
const matchingKeys = new Set(records.flatMap((record) => record.version === 2
|
|
61
|
+
&& record.source.project === project && record.source.projectKey ? [record.source.projectKey] : []));
|
|
62
|
+
for (const [key, binding] of Object.entries(config.teamSpaces ?? {})) {
|
|
63
|
+
if (PROJECT_KEY_RE.test(key) && binding.localConfirmed === true && binding.localProject === project) {
|
|
64
|
+
matchingKeys.add(key);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (matchingKeys.size > 1) {
|
|
68
|
+
return { records, note: `More than one stable project is named ${project}; supply projectKey so their memories are not combined.` };
|
|
69
|
+
}
|
|
70
|
+
const exactProjectKey = [...matchingKeys][0];
|
|
71
|
+
const unconfirmedBinding = Object.entries(config.teamSpaces ?? {}).some(([key, binding]) => binding.localConfirmed !== true && (key === project || binding.localProject === project));
|
|
72
|
+
if (!exactProjectKey && unconfirmedBinding) {
|
|
73
|
+
return { records, note: `No locally confirmed project identity for ${project}; supply project and projectKey from a confirmed checkout.` };
|
|
74
|
+
}
|
|
75
|
+
// No binding evidence is not a refusal: a fresh personal device can have a
|
|
76
|
+
// readable account index without any local archive records or team config.
|
|
77
|
+
return { records, projectKey: exactProjectKey };
|
|
78
|
+
}
|
|
79
|
+
async function defaultWorkspace(project, projectKey, scope) {
|
|
80
|
+
if (project || projectKey)
|
|
81
|
+
return { project, projectKey };
|
|
82
|
+
if (scope === "account")
|
|
83
|
+
return { project, projectKey };
|
|
84
|
+
const config = await readConfig(dataDir);
|
|
85
|
+
const bindings = config.teamSpaces ?? {};
|
|
86
|
+
// A confirmed team project is the ONLY thing that makes an unqualified search
|
|
87
|
+
// ambiguous: which member's project did the agent mean? With none, this is a
|
|
88
|
+
// personal vault, and its own account history is unambiguously the answer.
|
|
89
|
+
// Returning the "resolve a project first" note here instead handed an empty
|
|
90
|
+
// result to the single most important question — "what did I decide before" —
|
|
91
|
+
// which reads exactly like "you never discussed that", the failure the phrase
|
|
92
|
+
// path exists to prevent. So default to account scope unless a real team
|
|
93
|
+
// project could be confused for another.
|
|
94
|
+
const hasConfirmedProject = Object.entries(bindings).some(([key, binding]) => /^(g1|p1):[a-f0-9]{64}$/.test(key) && binding.localConfirmed && Boolean(binding.localProject));
|
|
95
|
+
if (!hasConfirmedProject)
|
|
96
|
+
return { project: undefined, projectKey: undefined };
|
|
97
|
+
let roots;
|
|
98
|
+
if (server.server.getClientCapabilities()?.roots) {
|
|
99
|
+
// Capability advertised but failed/empty roots is not permission to use a
|
|
100
|
+
// possibly unrelated process directory. Ask for an explicit scope instead.
|
|
101
|
+
roots = await server.server.listRoots(undefined, { timeout: 2000 }).then((result) => result.roots, () => []);
|
|
102
|
+
}
|
|
103
|
+
return resolveMcpWorkspace({ roots, cwd: process.cwd(), bindings });
|
|
104
|
+
}
|
|
105
|
+
async function requirePhrase() {
|
|
106
|
+
const phrase = await phraseAvailable();
|
|
107
|
+
if (!phrase)
|
|
108
|
+
throw new Error("No recovery phrase: set SEALKEEP_RECOVERY_PHRASE, or store the phrase in this machine's keystore (Settings → remember, or `sealkeep autopilot`)");
|
|
109
|
+
return phrase;
|
|
110
|
+
}
|
|
111
|
+
/** Registers one tool under one name. */
|
|
112
|
+
function registerTool(name, config,
|
|
113
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
114
|
+
handler) {
|
|
115
|
+
server.registerTool(name, config, handler);
|
|
116
|
+
}
|
|
117
|
+
registerTool("sealkeep_status", {
|
|
15
118
|
title: "Sealkeep status",
|
|
16
119
|
description: "Read local Sealkeep health, archive count, and encrypted storage usage. No session content is returned.",
|
|
17
120
|
inputSchema: {}
|
|
18
121
|
}, async () => text(await vaultStatus(dataDir)));
|
|
19
|
-
|
|
122
|
+
/**
|
|
123
|
+
* The one tool that justifies an agent having this server at all.
|
|
124
|
+
*
|
|
125
|
+
* It used to substring-match the archive id, the source path and the agent
|
|
126
|
+
* name — which, for a Codex rollout named `rollout-<timestamp>-<uuid>.jsonl`,
|
|
127
|
+
* means an agent could search its own history for a date or a UUID it has no
|
|
128
|
+
* way of knowing. Nothing it would actually ask for could ever match. The
|
|
129
|
+
* capability existed the whole time in the CLI; it was simply never wired to
|
|
130
|
+
* the surface where an agent could reach it.
|
|
131
|
+
*
|
|
132
|
+
* Content mode decrypts locally, needs the recovery phrase, and is the
|
|
133
|
+
* default when one is available — because "what did I decide about retention
|
|
134
|
+
* last week" is the question, and metadata cannot answer it. Without a phrase
|
|
135
|
+
* it degrades to the old metadata match and says so, rather than returning an
|
|
136
|
+
* empty list that reads like "you never discussed that".
|
|
137
|
+
*/
|
|
138
|
+
registerTool("sealkeep_search", {
|
|
20
139
|
title: "Search archived sessions",
|
|
21
|
-
description: "Searches
|
|
22
|
-
inputSchema: {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
140
|
+
description: "Searches archived sessions and returns matching quotes plus immutable source/context references. To understand a found decision, pass the hit's context object to sealkeep_read_context; it expands the source, user prompt and neighboring messages without repeated searches. With no recovery phrase, only metadata is searched. Decryption is local.",
|
|
141
|
+
inputSchema: {
|
|
142
|
+
query: z.string().min(1).max(200),
|
|
143
|
+
agent: z.string().optional(),
|
|
144
|
+
archiveId: z.string().uuid().optional()
|
|
145
|
+
.describe("Search only one immutable archive snapshot. This remains available for callers that already know an archive id."),
|
|
146
|
+
sessionId: z.string().uuid().optional()
|
|
147
|
+
.describe("Search every preserved snapshot of this exact Codex session; resume pointers supply it automatically."),
|
|
148
|
+
project: z.string().optional().describe("Explicit project override. Otherwise resolve the current client workspace to its confirmed stable project."),
|
|
149
|
+
scope: z.enum(["workspace", "account"]).optional().describe("Default workspace. Use account only to explicitly search across personal account history without a project filter."),
|
|
150
|
+
projectKey: z.string().regex(/^(?:g1|p1):[a-f0-9]{64}$/).optional()
|
|
151
|
+
.describe("Stable project ref supplied by Sealkeep context; use with project when same-named folders exist."),
|
|
152
|
+
mode: z.enum(["content", "metadata"]).optional()
|
|
153
|
+
}
|
|
154
|
+
}, async ({ query, agent, archiveId, sessionId, project, projectKey, mode, scope }) => {
|
|
155
|
+
const phrase = await phraseAvailable();
|
|
156
|
+
const wanted = mode ?? (phrase ? "content" : "metadata");
|
|
157
|
+
const workspace = await defaultWorkspace(project, projectKey, scope);
|
|
158
|
+
if ("note" in workspace)
|
|
159
|
+
return text({ mode: wanted, ...workspace, hits: [] });
|
|
160
|
+
({ project, projectKey } = workspace);
|
|
161
|
+
const resolved = await resolveProject(project, projectKey);
|
|
162
|
+
if (resolved.note)
|
|
163
|
+
return text({ mode: wanted, note: resolved.note, hits: [] });
|
|
164
|
+
const { records, projectKey: exactProjectKey } = resolved;
|
|
165
|
+
if (wanted === "metadata" || !phrase) {
|
|
166
|
+
const hits = await search(dataDir, query, {
|
|
167
|
+
mode: "metadata",
|
|
168
|
+
...(agent ? { agent } : {}),
|
|
169
|
+
...(archiveId ? { archiveId } : {}),
|
|
170
|
+
...(sessionId ? { sessionId } : {}),
|
|
171
|
+
});
|
|
172
|
+
const scoped = exactProjectKey
|
|
173
|
+
? hits.filter((hit) => records.some((record) => record.id === hit.id && record.version === 2 && record.source.projectKey === exactProjectKey))
|
|
174
|
+
: project
|
|
175
|
+
? hits.filter((hit) => records.some((record) => record.id === hit.id && record.version === 2
|
|
176
|
+
&& !record.source.projectKey && record.source.project === project))
|
|
177
|
+
: hits;
|
|
178
|
+
return text({
|
|
179
|
+
mode: "metadata",
|
|
180
|
+
...(!phrase ? { note: "No recovery phrase on this machine: store it in the keystore (Settings → remember) or set SEALKEEP_RECOVERY_PHRASE; matched on file metadata only." } : {}),
|
|
181
|
+
hits: scoped,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
const hits = await search(dataDir, query, {
|
|
185
|
+
mode: wanted,
|
|
186
|
+
...(phrase ? { phrase } : {}),
|
|
187
|
+
...(agent ? { agent } : {}),
|
|
188
|
+
...(archiveId ? { archiveId } : {}),
|
|
189
|
+
...(sessionId ? { sessionId } : {}),
|
|
190
|
+
...(project ? { project } : {}),
|
|
191
|
+
...(exactProjectKey ? { projectKey: exactProjectKey } : {}),
|
|
192
|
+
});
|
|
193
|
+
return text({ mode: wanted, hits, next: "Pass a hit's context object to sealkeep_read_context to read its decision and surrounding messages." });
|
|
194
|
+
});
|
|
195
|
+
registerTool("sealkeep_read_context", {
|
|
196
|
+
title: "Read context around a search result",
|
|
197
|
+
description: "Pass a search hit's context object to read its immutable source passage, actual user prompt when available, nearby messages and tool relationships. Follow the returned continuation as cursor with the same source to read more. Each call checks current Team access and enforces transfer, decompression and response budgets. This is a local read; it writes no restored transcript.",
|
|
198
|
+
inputSchema: {
|
|
199
|
+
source: sourceSchema, query: z.string().max(200).optional(),
|
|
200
|
+
byteStart: z.number().int().nonnegative().optional(), chunkIndex: z.number().int().nonnegative().optional(),
|
|
201
|
+
cursor: z.object({ sourceSha256: z.string().regex(/^[a-f0-9]{64}$/i), byteStart: z.number().int().nonnegative(), textOffset: z.number().int().nonnegative().optional() }).optional(),
|
|
202
|
+
maxTokens: z.number().int().min(2048).max(32768).optional().describe("Default 8192. Conservative UTF-8 byte ceiling for the complete JSON response, including provenance and continuation; this safely bounds text tokens."),
|
|
203
|
+
maxWireBytes: z.number().int().min(1).max(64 * 1024 * 1024).optional().describe("Default 8 MiB. Aggregate encrypted index and archive payload transfers; small authorization responses are excluded. The buffered index must also fit this ceiling even when cached."),
|
|
204
|
+
maxDecodedBytes: z.number().int().min(1).max(64 * 1024 * 1024).optional().describe("Default 16 MiB. Aggregate decoded index/source-facts plus source/dependency bytes, including chunk padding and rejected cache decode attempts. Team source-facts cache hits decode only generation-bound facts after current head/access checks; a miss verifies the whole checkpoint. index_budget_exceeded means checkpoint work exceeds the budget, not that the cited passage is absent."),
|
|
205
|
+
neighboringMessages: z.number().int().min(0).max(12).optional(),
|
|
206
|
+
}, annotations: { readOnlyHint: true },
|
|
207
|
+
}, async (args) => text(await readContext(dataDir, await requirePhrase(), { ...args, signal: AbortSignal.timeout(30_000) }), true));
|
|
208
|
+
registerTool("sealkeep_prepare_archive", {
|
|
29
209
|
title: "Archive a session",
|
|
30
210
|
description: "Encrypts and queues a local transcript for archive. The transcript is encrypted before it reaches any storage provider.",
|
|
31
211
|
inputSchema: { path: z.string().min(1), agent: z.string().default("custom") },
|
|
32
212
|
annotations: { destructiveHint: false }
|
|
33
|
-
}, async ({ path, agent }) => text(await archiveFile(dataDir, path, requirePhrase(), agent)));
|
|
34
|
-
|
|
213
|
+
}, async ({ path, agent }) => text(await archiveFile(dataDir, path, await requirePhrase(), agent, { indexInline: true })));
|
|
214
|
+
registerTool("sealkeep_recover", {
|
|
35
215
|
title: "Recover an archived session",
|
|
36
|
-
description: "
|
|
216
|
+
description: "Dry-run by default: reports the archive, byte count, and destination a restore WOULD produce, writing nothing — pass dryRun: false to actually decrypt into the destination path. For a Team result, include the search hit's source reference or its project/projectKey. Team restores currently support up to 64 MiB of transferred and decoded bytes; larger sources report an explicit unsupported limit. Use sealkeep_read_context for a bounded reading without restoring a file. Existing files are refused by default; backup/replace retains the displaced inode.",
|
|
37
217
|
inputSchema: {
|
|
38
218
|
archiveId: z.string().uuid(),
|
|
219
|
+
source: sourceSchema.optional(), project: z.string().optional(), projectKey: z.string().optional(),
|
|
39
220
|
destination: z.string().min(1).optional(),
|
|
40
221
|
native: z.boolean().default(false).describe("Restore to the adapter's original transcript path instead of a chosen destination"),
|
|
41
|
-
overwrite: z.enum(["refuse", "backup", "replace"]).default("refuse")
|
|
222
|
+
overwrite: z.enum(["refuse", "backup", "replace"]).default("refuse"),
|
|
223
|
+
dryRun: z.boolean().default(true).describe("Preview only (the default): nothing is written. Pass false to perform the restore.")
|
|
42
224
|
},
|
|
43
225
|
annotations: { destructiveHint: true }
|
|
44
|
-
}, async ({ archiveId, destination, native, overwrite }) => {
|
|
45
|
-
const outcome = await restoreArchive(dataDir, archiveId, requirePhrase(), { destination, native, overwrite });
|
|
46
|
-
return text({
|
|
226
|
+
}, async ({ archiveId, source, project, projectKey, destination, native, overwrite, dryRun }) => {
|
|
227
|
+
const outcome = await restoreArchive(dataDir, archiveId, await requirePhrase(), { source, project, projectKey, destination, native, overwrite, dryRun });
|
|
228
|
+
return text({
|
|
229
|
+
archiveId: outcome.record.id, output: outcome.output, bytes: outcome.bytes, native: outcome.native, backupPath: outcome.backupPath,
|
|
230
|
+
...(outcome.dryRun ? {
|
|
231
|
+
dryRun: true,
|
|
232
|
+
...(outcome.destinationExists !== undefined ? { destinationExists: outcome.destinationExists } : {}),
|
|
233
|
+
note: "Preview only — nothing was written. Call again with dryRun: false to restore.",
|
|
234
|
+
} : {}),
|
|
235
|
+
});
|
|
47
236
|
});
|
|
48
|
-
|
|
237
|
+
registerTool("sealkeep_create_upload", {
|
|
49
238
|
title: "Get upload lease status",
|
|
50
239
|
description: "Reports the storage lease mechanism used by Sealkeep. In the local MVP provider, no remote URL is necessary; the future hosted control plane returns a short-lived, object-scoped signed URL.",
|
|
240
|
+
// "vaultline" here is the built-in local storage provider's wire identifier, not the product
|
|
241
|
+
// name — every archive record and lease already on disk says "vaultline", so this id does not change.
|
|
51
242
|
inputSchema: { provider: z.enum(["vaultline", "s3", "r2", "gcs"]).default("vaultline"), bucket: z.string().min(1), prefix: z.string().min(1), ciphertextSha256: z.string().regex(/^[a-f0-9]{64}$/), bytes: z.number().int().positive() }
|
|
52
243
|
}, async ({ provider, bucket, prefix, ciphertextSha256, bytes }) => text(providers[provider].createUploadLease({ provider: provider, bucket, prefix }, { ciphertextSha256, bytes })));
|
|
53
|
-
|
|
244
|
+
registerTool("sealkeep_retention_preview", {
|
|
54
245
|
title: "Preview local session reclamation",
|
|
55
246
|
description: "Lists old source transcripts that could eventually be reclaimed. This is read-only and always preserves sources until a verified remote archive exists.",
|
|
56
247
|
inputSchema: { olderThanDays: z.number().min(0).default(30) },
|
|
57
248
|
annotations: { readOnlyHint: true }
|
|
58
249
|
}, async ({ olderThanDays }) => text(await previewRetention(dataDir, olderThanDays)));
|
|
250
|
+
/**
|
|
251
|
+
* Realtime Bridge — the tools that let a member's OWN agent follow a shared
|
|
252
|
+
* session live. `sealkeep_bridge_watch` advances this machine's mirror of a
|
|
253
|
+
* channel and hands back the freshly-arrived lines, so an agent can reason
|
|
254
|
+
* about a colleague's ongoing session ("what has Paul's run concluded so far?")
|
|
255
|
+
* while it is still running. `sealkeep_bridge_publish_pass` is the driver-side
|
|
256
|
+
* counterpart for a companion that wants to push the next delta by hand.
|
|
257
|
+
*
|
|
258
|
+
* Read-only over the session: neither tool can drive another person's agent.
|
|
259
|
+
* See docs/realtime-bridge.md for the scope ladder.
|
|
260
|
+
*/
|
|
261
|
+
registerTool("sealkeep_bridge_watch", {
|
|
262
|
+
title: "Follow a shared session (bridge)",
|
|
263
|
+
description: "Pulls any new sealed deltas of a live bridge channel, verifies the hash chain, and appends them to this machine's local mirror. Returns the mirror path and the newly-arrived text so your agent can follow a colleague's ongoing session. Needs the recovery phrase to unseal; content is decrypted locally.",
|
|
264
|
+
inputSchema: { channel: z.string().regex(/^[A-Za-z0-9-]{1,64}$/), fromSeq: z.number().int().min(0).default(0) },
|
|
265
|
+
annotations: { readOnlyHint: true }
|
|
266
|
+
}, async ({ channel, fromSeq }) => {
|
|
267
|
+
const phrase = await requirePhrase();
|
|
268
|
+
const mirrorPath = defaultMirrorPath(dataDir, channel);
|
|
269
|
+
const before = await readFile(mirrorPath).then((b) => b.length).catch(() => 0);
|
|
270
|
+
const result = await watchOnce(dataDir, { channel, fromSeq, mirrorPath }, phrase);
|
|
271
|
+
const after = await readFile(mirrorPath).catch(() => Buffer.alloc(0));
|
|
272
|
+
const fresh = after.subarray(before).toString("utf8");
|
|
273
|
+
return text({ channel, mirrorPath, applied: result.applied, nextSeq: result.nextSeq, gaps: result.gaps, needsReset: result.needsReset, newText: fresh });
|
|
274
|
+
});
|
|
275
|
+
registerTool("sealkeep_bridge_publish_pass", {
|
|
276
|
+
title: "Publish the next bridge delta (driver)",
|
|
277
|
+
description: "Driver side: seals the bytes a session file has grown by since a given offset and pushes them as the next delta on a bridge channel. Returns the new high-water offset to pass next time. The transcript is sealed on this machine before anything leaves it.",
|
|
278
|
+
inputSchema: { channel: z.string().regex(/^[A-Za-z0-9-]{1,64}$/), sessionPath: z.string().min(1), seq: z.number().int().min(0), fromByte: z.number().int().min(0).default(0) }
|
|
279
|
+
}, async ({ channel, sessionPath, seq, fromByte }) => {
|
|
280
|
+
const phrase = await requirePhrase();
|
|
281
|
+
const outcome = await publishOnce(dataDir, { channel, seq, sessionPath, fromByte }, phrase);
|
|
282
|
+
return text(outcome ? { published: outcome.ref, seq: outcome.seq, newHighWater: outcome.newHighWater } : { published: null, note: "Nothing new since fromByte." });
|
|
283
|
+
});
|
|
284
|
+
registerTool("sealkeep_team", {
|
|
285
|
+
title: "Who is working on what right now",
|
|
286
|
+
description: "The team awareness feed git lacks: who STARTED what (and has not finished), plus recent finished lines. Call this before taking a task that might overlap a teammate's work — if someone is already on it, coordinate instead of doubling the effort. Presence events are one sealed line each, published by launches and by active-mode seals.",
|
|
287
|
+
inputSchema: {
|
|
288
|
+
project: z.string().optional(),
|
|
289
|
+
scope: z.enum(["workspace", "account"]).optional(),
|
|
290
|
+
projectKey: z.string().regex(PROJECT_KEY_RE).optional()
|
|
291
|
+
.describe("Stable project ref; pair with project to distinguish same-named checkouts."),
|
|
292
|
+
},
|
|
293
|
+
annotations: { readOnlyHint: true }
|
|
294
|
+
}, async ({ project, projectKey, scope }) => {
|
|
295
|
+
const phrase = await requirePhrase();
|
|
296
|
+
const workspace = await defaultWorkspace(project, projectKey, scope);
|
|
297
|
+
if ("note" in workspace)
|
|
298
|
+
return text({ activeNow: [], recent: [], ...workspace });
|
|
299
|
+
({ project, projectKey } = workspace);
|
|
300
|
+
const resolved = await resolveProject(project, projectKey);
|
|
301
|
+
if (resolved.note)
|
|
302
|
+
return text({ activeNow: [], recent: [], note: resolved.note });
|
|
303
|
+
const { listPresence, activeNow } = await import("./presence.js");
|
|
304
|
+
const events = await listPresence(dataDir, phrase, { project, projectKey: resolved.projectKey, limit: 25 });
|
|
305
|
+
return text({ activeNow: activeNow(events), recent: events.slice(0, 12) });
|
|
306
|
+
});
|
|
307
|
+
registerTool("sealkeep_bridge_push", {
|
|
308
|
+
title: "Speak into a shared bridge channel",
|
|
309
|
+
description: "Sends a labeled message to every member of a live bridge channel — the collaboration write-path (anthropics/claude-code#60082): a follower's suggestion reaches the driver's agent on its next turn. Sealed on this machine before it leaves; kinds: suggest, note, handoff.",
|
|
310
|
+
inputSchema: { channel: z.string().regex(/^[A-Za-z0-9-]{1,64}$/), message: z.string().min(1), kind: z.enum(["suggest", "note", "handoff"]).default("suggest") }
|
|
311
|
+
}, async ({ channel, message, kind }) => {
|
|
312
|
+
const phrase = await requirePhrase();
|
|
313
|
+
const outcome = await publishBridgeMessage(dataDir, { channel, kind, text: message }, phrase);
|
|
314
|
+
return text({ pushed: outcome.ref, from: outcome.message.from, at: outcome.message.at });
|
|
315
|
+
});
|
|
316
|
+
registerTool("sealkeep_bridge_inbox", {
|
|
317
|
+
title: "Read teammates' bridge messages",
|
|
318
|
+
description: "Pulls new teammate messages on a bridge channel and returns the ones not yet delivered to this agent, oldest first, with a ready-to-use context block. This is the pull half of the collaboration lane — call it between tasks when following or driving a shared session (Codex drivers use this; Claude drivers usually get the same content injected by the UserPromptSubmit hook).",
|
|
319
|
+
inputSchema: { channel: z.string().regex(/^[A-Za-z0-9-]{1,64}$/) }
|
|
320
|
+
}, async ({ channel }) => {
|
|
321
|
+
const phrase = await requirePhrase();
|
|
322
|
+
await pullBridgeMessages(dataDir, channel, phrase);
|
|
323
|
+
const pending = await drainPending(dataDir, channel);
|
|
324
|
+
const { pendingCount } = await import("./bridge.js");
|
|
325
|
+
const waiting = await pendingCount(dataDir, channel);
|
|
326
|
+
return text({ channel, messages: pending, stillWaiting: waiting, context: promptInjection(pending, waiting) || null });
|
|
327
|
+
});
|
|
59
328
|
const transport = new StdioServerTransport();
|
|
60
329
|
server.connect(transport).catch((error) => { console.error(error); process.exit(1); });
|
package/dist/src/migrate.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { decryptArchive as openEnvelope, encryptArchive as sealEnvelope, rewrapArchive } from "../packages/
|
|
3
|
+
import { decryptArchive as openEnvelope, encryptArchive as sealEnvelope, rewrapArchive } from "../packages/sealkeep-crypto/src/index.js";
|
|
4
4
|
import { equalHex, matchesPhraseCheck, sha256 } from "./crypto.js";
|
|
5
5
|
import { canonicalPhrase } from "./mnemonic.js";
|
|
6
6
|
import { fail } from "./errors.js";
|
|
@@ -70,41 +70,47 @@ export async function rewrapVault(dataDir, rawPhrase, options = {}) {
|
|
|
70
70
|
}
|
|
71
71
|
if (!matchesPhraseCheck(config.recovery.phraseCheck, phrase))
|
|
72
72
|
fail("recovery_phrase_mismatch", "Recovery phrase does not match this vault");
|
|
73
|
-
const recipients = configuredRecipients(config, phrase, { group: options.group, project: options.project });
|
|
74
73
|
const records = await listArchives(dataDir);
|
|
75
74
|
const rewrapped = [];
|
|
76
75
|
const skipped = [];
|
|
77
76
|
// Extending a project's membership must not touch archives of other
|
|
78
77
|
// projects: rewrapping those would either add the member (leaking work they
|
|
79
78
|
// were never given) or drop them (silently revoking elsewhere).
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
79
|
+
const { projectOfRecord } = await import("./adapters.js");
|
|
80
|
+
/**
|
|
81
|
+
* Recipients are resolved PER RECORD, from that record's own project.
|
|
82
|
+
*
|
|
83
|
+
* One list computed for the whole vault dropped every project-scoped
|
|
84
|
+
* recipient the moment a rewrap ran without --project — which is the normal
|
|
85
|
+
* case, revoking an unrelated device or retiring a rotated phrase. Every
|
|
86
|
+
* project member then silently lost access to their project's existing
|
|
87
|
+
* archives, with a success message. The scoped-shard writer already resolved
|
|
88
|
+
* membership per project; this now does the same.
|
|
89
|
+
*/
|
|
90
|
+
const recipientsFor = new Map();
|
|
91
|
+
const listFor = (project) => {
|
|
92
|
+
const key = project ?? "";
|
|
93
|
+
let list = recipientsFor.get(key);
|
|
94
|
+
if (!list) {
|
|
95
|
+
list = configuredRecipients(config, phrase, { group: options.group, ...(project ? { project } : {}) });
|
|
96
|
+
recipientsFor.set(key, list);
|
|
97
|
+
}
|
|
98
|
+
return list;
|
|
99
|
+
};
|
|
95
100
|
for (const record of records) {
|
|
96
101
|
if (!isV2(record)) {
|
|
97
102
|
skipped.push({ id: record.id, reason: "v1 archive; run `sealkeep migrate` first" });
|
|
98
103
|
continue;
|
|
99
104
|
}
|
|
100
|
-
|
|
105
|
+
const recordProject = await projectOfRecord(record).catch(() => null);
|
|
106
|
+
if (options.project && recordProject !== options.project) {
|
|
101
107
|
skipped.push({ id: record.id, reason: `belongs to another project` });
|
|
102
108
|
continue;
|
|
103
109
|
}
|
|
104
|
-
const envelope = rewrapArchive(record.envelope, { phrase },
|
|
110
|
+
const envelope = rewrapArchive(record.envelope, { phrase }, listFor(recordProject));
|
|
105
111
|
await writeRecord(config.storage.root, { ...record, envelope });
|
|
106
112
|
rewrapped.push(record.id);
|
|
107
113
|
}
|
|
108
|
-
await recordAudit(dataDir, "archive.rewrap", "allowed", { rewrapped: rewrapped.length, skipped: skipped.length, recipients:
|
|
109
|
-
return { rewrapped: rewrapped.length, skipped, recipients:
|
|
114
|
+
await recordAudit(dataDir, "archive.rewrap", "allowed", { rewrapped: rewrapped.length, skipped: skipped.length, recipients: listFor(null).length, group: options.group ?? null });
|
|
115
|
+
return { rewrapped: rewrapped.length, skipped, recipients: listFor(null).length };
|
|
110
116
|
}
|
package/dist/src/notify.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export type TickSummary = {
|
|
|
33
33
|
* Decides whether a round of work is worth interrupting someone for.
|
|
34
34
|
*
|
|
35
35
|
* Routine sealing is not: it happens constantly and saying so would train people to
|
|
36
|
-
* ignore the product. Reclaimed disk is, because
|
|
36
|
+
* ignore the product. Reclaimed disk is, because local blocks were actually released. A failure is, because
|
|
37
37
|
* it needs a decision.
|
|
38
38
|
*/
|
|
39
39
|
export declare function summarize(tick: TickSummary): Notification | null;
|
package/dist/src/notify.js
CHANGED
|
@@ -53,7 +53,7 @@ function bytes(value) {
|
|
|
53
53
|
* Decides whether a round of work is worth interrupting someone for.
|
|
54
54
|
*
|
|
55
55
|
* Routine sealing is not: it happens constantly and saying so would train people to
|
|
56
|
-
* ignore the product. Reclaimed disk is, because
|
|
56
|
+
* ignore the product. Reclaimed disk is, because local blocks were actually released. A failure is, because
|
|
57
57
|
* it needs a decision.
|
|
58
58
|
*/
|
|
59
59
|
export function summarize(tick) {
|
|
@@ -62,11 +62,11 @@ export function summarize(tick) {
|
|
|
62
62
|
}
|
|
63
63
|
if (tick.reclaimed > 0) {
|
|
64
64
|
const freed = tick.freedBytes ? ` and freed ${bytes(tick.freedBytes)}` : "";
|
|
65
|
-
return { title: "Disk reclaimed", body: `
|
|
66
|
-
}
|
|
67
|
-
if (tick.archived >= 5) {
|
|
68
|
-
return { title: "Sessions preserved", body: `Sealed ${tick.archived} agent sessions${tick.uploaded ? `, ${tick.uploaded} copied off this machine` : ""}.` };
|
|
65
|
+
return { title: "Disk reclaimed", body: `Freed the local originals for ${tick.reclaimed} archived session${tick.reclaimed === 1 ? "" : "s"}${freed}. Their verified encrypted archives remain stored.` };
|
|
69
66
|
}
|
|
67
|
+
// Routine sealing never notifies. The docstring above says exactly that —
|
|
68
|
+
// "never for routine sealing, which would just train you to ignore it" — and
|
|
69
|
+
// a branch three lines below it fired on five ordinary seals anyway.
|
|
70
70
|
return null;
|
|
71
71
|
}
|
|
72
72
|
/** Keeps a quiet period between notifications so a busy machine cannot spam. */
|