sealkeep 0.11.4 → 0.11.5
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/CHANGELOG.md +28 -0
- package/dist/src/adapters.d.ts +1 -1
- package/dist/src/adapters.js +15 -16
- package/dist/src/agent-context.d.ts +2 -0
- package/dist/src/agent-context.js +18 -1
- package/dist/src/cli.js +9 -0
- package/dist/src/daemon.js +51 -1
- package/dist/src/local-api.js +5 -2
- package/dist/src/machine-settings.d.ts +13 -0
- package/dist/src/machine-settings.js +5 -2
- package/dist/src/storage-reachability.d.ts +9 -0
- package/dist/src/storage-reachability.js +38 -0
- package/package.json +1 -1
- package/web/app.js +2 -1
- package/web/index.html +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,34 @@
|
|
|
3
3
|
Notable changes, by published version. Sealkeep is pre-1.0: minor versions
|
|
4
4
|
may change behavior, and say so here when they do.
|
|
5
5
|
|
|
6
|
+
## 0.11.5 — 2026-09-23 — compacting is safe, and a lost destination says so
|
|
7
|
+
|
|
8
|
+
- **An agent that has just been compacted is told where the work stands.**
|
|
9
|
+
Compacting is the only thing that makes a running session cheaper: the whole
|
|
10
|
+
conversation is re-sent on every request, so cost is roughly its size times
|
|
11
|
+
the number of steps. Measured on one real machine, a session was sending
|
|
12
|
+
577,000 tokens per step, 98% of it re-read history. What stops people
|
|
13
|
+
compacting early is that the summary loses detail and the agent then
|
|
14
|
+
re-reads files to work out where it was, which costs far more than being
|
|
15
|
+
told. Sealkeep now hears the compaction — Claude names that source too, and
|
|
16
|
+
only Codex was listening — and treats the session as a new reader, because
|
|
17
|
+
it is: everything it had been handed is gone from the window. It says the
|
|
18
|
+
short version once, with your next message. Nothing is lost from the archive
|
|
19
|
+
either way; the transcript on disk keeps every message, compacted or not.
|
|
20
|
+
Settings → "What reaches your agent" has the switch. Codex reviews changed
|
|
21
|
+
hooks once, so it asks you to review Sealkeep's again after this upgrade.
|
|
22
|
+
|
|
23
|
+
- **When a destination stops accepting this machine, you are told.** Sealkeep
|
|
24
|
+
worked out reachability every minute and published it to the account panel,
|
|
25
|
+
so a Drive whose authorization had lapsed was visible to anyone who opened
|
|
26
|
+
Settings or ran doctor, and to nobody else — while archives piled up locally
|
|
27
|
+
and the upload ledger grew. It now says so once when that changes, as a
|
|
28
|
+
desktop notification on macOS, Linux and Windows, and as a line after your
|
|
29
|
+
next command. The first sight of a destination is never announced, still
|
|
30
|
+
broken is not repeated, and the last state is kept across restarts so an
|
|
31
|
+
upgrade neither repeats a message nor swallows one. It names what it means
|
|
32
|
+
for the work first: new archives stay here and nothing is lost.
|
|
33
|
+
|
|
6
34
|
## 0.11.4 — 2026-09-23 — history stays findable
|
|
7
35
|
|
|
8
36
|
- **An archive can no longer be dropped from search because one sidecar could
|
package/dist/src/adapters.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export type AgentInstall = {
|
|
|
26
26
|
export declare const SETUP_AGENT_IDS: SetupAgentId[];
|
|
27
27
|
/** The subset with a session adapter. Mirrors `AgentId` at runtime for schema use. */
|
|
28
28
|
export declare const ARCHIVING_AGENT_IDS: readonly ["codex", "claude"];
|
|
29
|
-
export declare const HOOK_LIFECYCLE_VERSION =
|
|
29
|
+
export declare const HOOK_LIFECYCLE_VERSION = 4;
|
|
30
30
|
/** Narrows a setup id to an archiving one, so callers prove the subset rather than casting to it. */
|
|
31
31
|
export declare function isArchivingAgent(agent: SetupAgentId): agent is AgentId;
|
|
32
32
|
/** Detect only well-known local roots; it never reads transcript contents. */
|
package/dist/src/adapters.js
CHANGED
|
@@ -69,7 +69,7 @@ const AGENTS = {
|
|
|
69
69
|
export const SETUP_AGENT_IDS = Object.keys(AGENTS);
|
|
70
70
|
/** The subset with a session adapter. Mirrors `AgentId` at runtime for schema use. */
|
|
71
71
|
export const ARCHIVING_AGENT_IDS = ["codex", "claude"];
|
|
72
|
-
export const HOOK_LIFECYCLE_VERSION =
|
|
72
|
+
export const HOOK_LIFECYCLE_VERSION = 4;
|
|
73
73
|
/** Narrows a setup id to an archiving one, so callers prove the subset rather than casting to it. */
|
|
74
74
|
export function isArchivingAgent(agent) {
|
|
75
75
|
return ARCHIVING_AGENT_IDS.includes(agent);
|
|
@@ -589,7 +589,10 @@ export function hookConfig(agent, executable = "sealkeep", dataDir = "~/.sealkee
|
|
|
589
589
|
...(agent === "codex" ? { additionalContextLimit: 2000 } : {})
|
|
590
590
|
};
|
|
591
591
|
const contextStart = [{
|
|
592
|
-
|
|
592
|
+
// "compact" matters most of all: the window has just been replaced by a
|
|
593
|
+
// summary, which is exactly when an agent starts re-reading files to work
|
|
594
|
+
// out where it was. Claude names that source too; only Codex was listening.
|
|
595
|
+
matcher: "startup|resume|clear|compact",
|
|
593
596
|
hooks: [{ type: "command", command: contextSync, timeout: 30, async: true }]
|
|
594
597
|
}];
|
|
595
598
|
const contextPrompt = [{ hooks: [
|
|
@@ -835,21 +838,17 @@ function lifecycleInstalled(agent, contents) {
|
|
|
835
838
|
const owned = commandsIn(document.hooks).filter((command) => lifecycleIdentity(command)?.endsWith(`:${agent}`));
|
|
836
839
|
if (owned.some((command) => !command.includes(`--lifecycle ${HOOK_LIFECYCLE_VERSION}`)))
|
|
837
840
|
return false;
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
841
|
+
// The expected lifecycle is read from the generator, not restated here. A
|
|
842
|
+
// restated count ("context-sync appears three times") declared every file
|
|
843
|
+
// the generator had just written incomplete the moment it gained a hook.
|
|
844
|
+
const expected = hookConfig(agent).hooks;
|
|
845
|
+
const identities = (commands) => commands.map(lifecycleIdentity).filter((identity) => identity !== null).sort();
|
|
846
|
+
if (identities(owned).join() !== identities(commandsIn(expected)).join())
|
|
843
847
|
return false;
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
&& eventHas("PostToolUse", "context")
|
|
849
|
-
&& eventHas("SessionStart", "context-sync")
|
|
850
|
-
&& eventHas("UserPromptSubmit", "context-sync")
|
|
851
|
-
&& eventHas("PostToolUse", "context-sync")
|
|
852
|
-
&& (agent !== "codex" || eventHas("PostCompact", "enqueue"));
|
|
848
|
+
return Object.entries(expected).every(([event, groups]) => {
|
|
849
|
+
const present = new Set(identities(commandsIn(document.hooks?.[event])));
|
|
850
|
+
return identities(commandsIn(groups)).every((identity) => present.has(identity));
|
|
851
|
+
});
|
|
853
852
|
}
|
|
854
853
|
catch {
|
|
855
854
|
return false;
|
|
@@ -23,6 +23,8 @@ export type AgentHookPayload = {
|
|
|
23
23
|
transcript_path?: string;
|
|
24
24
|
rollout_path?: string;
|
|
25
25
|
tool_name?: string;
|
|
26
|
+
/** Why a session started: "startup" | "resume" | "clear" | "compact". */
|
|
27
|
+
source?: string;
|
|
26
28
|
};
|
|
27
29
|
export type AutomaticContextOutcome = {
|
|
28
30
|
project: string;
|
|
@@ -88,6 +88,9 @@ export function parseAgentHookPayload(raw) {
|
|
|
88
88
|
hook_event_name: text("hook_event_name"), session_id: text("session_id"), thread_id: text("thread_id"),
|
|
89
89
|
cwd: text("cwd"), prompt: text("prompt"), transcript_path: text("transcript_path"),
|
|
90
90
|
rollout_path: text("rollout_path"), tool_name: text("tool_name"),
|
|
91
|
+
// Without this a compaction is indistinguishable from any other start,
|
|
92
|
+
// and the session is never told what the summary dropped.
|
|
93
|
+
source: text("source"),
|
|
91
94
|
};
|
|
92
95
|
}
|
|
93
96
|
catch {
|
|
@@ -1122,6 +1125,20 @@ export async function automaticAgentContext(dataDir, phrase, agent, payload, opt
|
|
|
1122
1125
|
//
|
|
1123
1126
|
// SEALKEEP_RECALL_PUSH=0 turns later boundaries off entirely, leaving only
|
|
1124
1127
|
// the memories handed over at session start.
|
|
1128
|
+
// A compaction replaces the conversation with a summary, so everything this
|
|
1129
|
+
// session was already handed is gone from the window — and the record of
|
|
1130
|
+
// what it has been told is now wrong. Clearing that record re-establishes
|
|
1131
|
+
// the framing and the load-bearing lines once, at the one moment an agent
|
|
1132
|
+
// would otherwise start re-reading files to work out where it was. That
|
|
1133
|
+
// re-read costs far more than the few hundred tokens this spends, and
|
|
1134
|
+
// compaction is the only thing that makes a running session cheaper: the
|
|
1135
|
+
// whole window is re-sent on every request, so cost is roughly its size
|
|
1136
|
+
// times the number of steps. Nothing is lost from the archive either way —
|
|
1137
|
+
// the transcript on disk keeps every message, compacted or not.
|
|
1138
|
+
const settings = await readLocalSettings(dataDir).catch(() => null);
|
|
1139
|
+
if (event === "SessionStart" && payload.source === "compact" && (settings?.recall.atCompaction ?? true)) {
|
|
1140
|
+
state.injected = [];
|
|
1141
|
+
}
|
|
1125
1142
|
const seen = new Set(state.injected ?? []);
|
|
1126
1143
|
const greeted = seen.size > 0;
|
|
1127
1144
|
// The switch lives in Settings; the environment variable stays as an override
|
|
@@ -1131,7 +1148,7 @@ export async function automaticAgentContext(dataDir, phrase, agent, payload, opt
|
|
|
1131
1148
|
? true
|
|
1132
1149
|
: quiet === "1" || quiet === "true" || quiet === "on"
|
|
1133
1150
|
? false
|
|
1134
|
-
: !(
|
|
1151
|
+
: !(settings?.recall.autoInject ?? true);
|
|
1135
1152
|
// History the agent will actually believe goes in its own memory directory,
|
|
1136
1153
|
// not only into the prompt. See src/agent-memory.ts for why: injected recall
|
|
1137
1154
|
// was delivered perfectly and then refused, on two different models.
|
package/dist/src/cli.js
CHANGED
|
@@ -3778,6 +3778,15 @@ async function noteNewerRelease() {
|
|
|
3778
3778
|
const latest = await newerReleaseThan(current, dataDir);
|
|
3779
3779
|
if (latest)
|
|
3780
3780
|
console.error(`\n ${hint(updateNotice(latest, current))}\n`);
|
|
3781
|
+
// A destination that stopped accepting this machine is worth the same
|
|
3782
|
+
// sentence, and for the same reason: a person finds out from the tool they
|
|
3783
|
+
// already have open, or not at all. The background service notices within
|
|
3784
|
+
// a minute and records it; this only reads that record, so it costs no
|
|
3785
|
+
// network and no credentials.
|
|
3786
|
+
const { unreachableDestinations, unreachableNotice } = await import("./storage-reachability.js");
|
|
3787
|
+
const unreachable = await unreachableDestinations(dataDir);
|
|
3788
|
+
if (unreachable.length)
|
|
3789
|
+
console.error(`\n ${hint(unreachableNotice(unreachable))}\n`);
|
|
3781
3790
|
}
|
|
3782
3791
|
catch { /* a version check must never be the thing that fails */ }
|
|
3783
3792
|
}
|
package/dist/src/daemon.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
1
3
|
import { freeBytes, resolveReserveBytes } from "./disk.js";
|
|
2
4
|
import { ArchiveQueue } from "./queue.js";
|
|
3
5
|
import { applyRetention, readApprovals, reconcileCodexReclaims, retentionSettings } from "./retention.js";
|
|
@@ -670,10 +672,58 @@ export async function startDaemon(dataDir, options) {
|
|
|
670
672
|
// stay on each machine. Report only reachability (never credentials) on an
|
|
671
673
|
// independent advisory lane so a disconnected Drive is visible in the
|
|
672
674
|
// account panel before the next archive tries and fails to upload.
|
|
675
|
+
/**
|
|
676
|
+
* Say it out loud when a destination stops accepting this machine.
|
|
677
|
+
*
|
|
678
|
+
* Reachability was computed every minute and published to the account panel,
|
|
679
|
+
* and that was all: a Drive whose access had lapsed was visible only if you
|
|
680
|
+
* opened Settings or ran doctor. Meanwhile archives pile up locally and the
|
|
681
|
+
* upload ledger grows — the silent stall this product exists to prevent.
|
|
682
|
+
* Only a change is announced, and the last state is kept on disk so a
|
|
683
|
+
* restart (an upgrade restarts this service) neither repeats the message nor
|
|
684
|
+
* swallows one that happened while it was down.
|
|
685
|
+
*/
|
|
686
|
+
const { reachabilityPath } = await import("./storage-reachability.js");
|
|
687
|
+
const reachabilityFile = reachabilityPath(dataDir);
|
|
688
|
+
const readReachability = async () => {
|
|
689
|
+
try {
|
|
690
|
+
const raw = JSON.parse(await readFile(reachabilityFile, "utf8"));
|
|
691
|
+
return Object.fromEntries(Object.entries(raw).filter(([, value]) => typeof value === "boolean"));
|
|
692
|
+
}
|
|
693
|
+
catch {
|
|
694
|
+
return {};
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
const announceStorageReachability = async () => {
|
|
698
|
+
const { resolveTargets, targetConnectionEvidence } = await import("./storage-targets.js");
|
|
699
|
+
const previous = await readReachability();
|
|
700
|
+
const current = {};
|
|
701
|
+
const changes = [];
|
|
702
|
+
for (const target of await resolveTargets(dataDir)) {
|
|
703
|
+
if (target.enabled === false)
|
|
704
|
+
continue;
|
|
705
|
+
const { connected } = await targetConnectionEvidence(dataDir, target).catch(() => ({ connected: previous[target.id] ?? true }));
|
|
706
|
+
current[target.id] = connected;
|
|
707
|
+
const was = previous[target.id];
|
|
708
|
+
// An unseen destination is recorded, never announced: the first sight of
|
|
709
|
+
// one is not news, and a new machine would otherwise open with a warning.
|
|
710
|
+
if (was !== undefined && was !== connected)
|
|
711
|
+
changes.push({ target: target.provider ?? target.id, connected });
|
|
712
|
+
}
|
|
713
|
+
await mkdir(dirname(reachabilityFile), { recursive: true, mode: 0o700 }).catch(() => undefined);
|
|
714
|
+
await writeFile(reachabilityFile, JSON.stringify(current, null, 2) + "\n", { mode: 0o600 }).catch(() => undefined);
|
|
715
|
+
for (const change of changes) {
|
|
716
|
+
await notify(change.connected
|
|
717
|
+
? { title: "Storage reconnected", body: `${change.target} is accepting this machine again. Waiting archives upload on the next pass.` }
|
|
718
|
+
: { title: "Sealkeep cannot reach your storage", body: `${change.target} is not accepting this machine. New archives stay here, nothing is lost, and Sealkeep keeps trying. Reconnect it in Settings.` }, { ...options.notify, enabled: options.notifications ?? options.notify?.enabled }).catch(() => undefined);
|
|
719
|
+
}
|
|
720
|
+
};
|
|
673
721
|
const reportStorageHealth = async () => {
|
|
674
722
|
if (storageHealthInFlight)
|
|
675
723
|
return storageHealthInFlight;
|
|
676
|
-
const task = import("./storage-targets.js")
|
|
724
|
+
const task = import("./storage-targets.js")
|
|
725
|
+
.then(({ pushTargetUsage }) => pushTargetUsage(dataDir))
|
|
726
|
+
.then(() => announceStorageReachability().catch(() => undefined));
|
|
677
727
|
storageHealthInFlight = task;
|
|
678
728
|
try {
|
|
679
729
|
await task;
|
package/dist/src/local-api.js
CHANGED
|
@@ -214,7 +214,7 @@ const localSettingsSchema = z.object({
|
|
|
214
214
|
bandwidth: z.object({ uploadLimitMbps: z.number().min(0).max(100_000).nullable() }).strict().optional(),
|
|
215
215
|
// What reaches the agent. Injection is convenient and it is re-read on every
|
|
216
216
|
// later step of a conversation, so it is a switch rather than a given.
|
|
217
|
-
recall: z.object({ autoInject: z.boolean() }).strict().optional(),
|
|
217
|
+
recall: z.object({ autoInject: z.boolean(), atCompaction: z.boolean().optional() }).strict().optional(),
|
|
218
218
|
mcp: z.object({ enabled: z.boolean() }).strict().optional(),
|
|
219
219
|
// Ceilings for background work. A floor on each, because a limit below it
|
|
220
220
|
// would stall the work rather than pace it. null returns to automatic.
|
|
@@ -5474,7 +5474,10 @@ export function createLocalApiServer(dataDir, token, options = {}) {
|
|
|
5474
5474
|
const next = {
|
|
5475
5475
|
...current, ...patch,
|
|
5476
5476
|
schedule: { ...current.schedule, ...patch.schedule },
|
|
5477
|
-
bandwidth: { ...current.bandwidth, ...patch.bandwidth }
|
|
5477
|
+
bandwidth: { ...current.bandwidth, ...patch.bandwidth },
|
|
5478
|
+
// Merged field by field like the others: a panel that sends only the
|
|
5479
|
+
// switch it changed must not drop the rest of the group.
|
|
5480
|
+
recall: { ...current.recall, ...patch.recall },
|
|
5478
5481
|
};
|
|
5479
5482
|
// The explicit, confirmed machine opt-in must be operational rather
|
|
5480
5483
|
// than a green switch in front of a policy that can never select work.
|
|
@@ -32,6 +32,19 @@ export type LocalSettings = {
|
|
|
32
32
|
*/
|
|
33
33
|
recall: {
|
|
34
34
|
autoInject: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Hand the agent its bearings back when its conversation is compacted.
|
|
37
|
+
*
|
|
38
|
+
* Compaction is the only thing that makes a running session cheaper: the
|
|
39
|
+
* whole conversation is re-sent on every request, so cost is roughly the
|
|
40
|
+
* size of the live window times the number of steps. What stops people
|
|
41
|
+
* compacting early is that the summary loses detail and the agent then
|
|
42
|
+
* re-reads files to work out where it was — which costs far more than the
|
|
43
|
+
* few hundred tokens of a note saying what is true now and that the rest
|
|
44
|
+
* is one search away. Nothing is lost from the archive either way: the
|
|
45
|
+
* transcript on disk keeps every message, compacted or not.
|
|
46
|
+
*/
|
|
47
|
+
atCompaction: boolean;
|
|
35
48
|
};
|
|
36
49
|
/** The local MCP server that lets an agent search this vault by itself. */
|
|
37
50
|
mcp: {
|
|
@@ -113,7 +113,7 @@ export function defaultLocalSettings() {
|
|
|
113
113
|
// On by default: an agent that is handed its history at the start of a
|
|
114
114
|
// session is the product. It is deduplicated and never repeats itself, so
|
|
115
115
|
// the standing cost is small — but it is a cost, and it is switchable.
|
|
116
|
-
recall: { autoInject: true },
|
|
116
|
+
recall: { autoInject: true, atCompaction: true },
|
|
117
117
|
mcp: { enabled: true },
|
|
118
118
|
limits: { cpuPercent: null, memoryMb: null },
|
|
119
119
|
};
|
|
@@ -151,7 +151,10 @@ export async function readLocalSettings(dataDir) {
|
|
|
151
151
|
exclusions: Array.isArray(saved?.exclusions) ? saved.exclusions.filter((path) => typeof path === "string") : [],
|
|
152
152
|
schedule: { ...fallback.schedule, ...saved?.schedule },
|
|
153
153
|
bandwidth: { ...fallback.bandwidth, ...saved?.bandwidth },
|
|
154
|
-
recall: {
|
|
154
|
+
recall: {
|
|
155
|
+
autoInject: typeof saved?.recall?.autoInject === "boolean" ? saved.recall.autoInject : fallback.recall.autoInject,
|
|
156
|
+
atCompaction: typeof saved?.recall?.atCompaction === "boolean" ? saved.recall.atCompaction : fallback.recall.atCompaction,
|
|
157
|
+
},
|
|
155
158
|
mcp: { enabled: typeof saved?.mcp?.enabled === "boolean" ? saved.mcp.enabled : fallback.mcp.enabled },
|
|
156
159
|
limits: {
|
|
157
160
|
// A floor of 5% and 128 MB, because a ceiling below those would stall
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const reachabilityPath: (dataDir: string) => string;
|
|
2
|
+
/** Destination ids the service last saw refusing this machine, sorted. */
|
|
3
|
+
export declare function unreachableDestinations(dataDir: string): Promise<string[]>;
|
|
4
|
+
/**
|
|
5
|
+
* Says what broke, what it means for the work, and what to do — in that order.
|
|
6
|
+
* "Nothing is lost" is the part a person needs first: the archives are still
|
|
7
|
+
* here and still sealed, they simply have not left this machine yet.
|
|
8
|
+
*/
|
|
9
|
+
export declare function unreachableNotice(names: readonly string[]): string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which destinations last refused this machine, and how to say so.
|
|
3
|
+
*
|
|
4
|
+
* Reachability was computed every minute and published to the account panel,
|
|
5
|
+
* and that was the only place it went: a Drive whose authorization had lapsed
|
|
6
|
+
* was visible to someone who opened Settings or ran doctor, and to nobody
|
|
7
|
+
* else. Archives pile up locally meanwhile and the upload ledger grows, which
|
|
8
|
+
* is the silent stall this product exists to prevent.
|
|
9
|
+
*
|
|
10
|
+
* The background service owns the observation and writes it here. Every reader
|
|
11
|
+
* — the CLI notice, a test — only reads this file, so telling a person costs
|
|
12
|
+
* no network call and touches no credential.
|
|
13
|
+
*/
|
|
14
|
+
import { readFile } from "node:fs/promises";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
export const reachabilityPath = (dataDir) => join(dataDir, "runtime", "storage-reachability.json");
|
|
17
|
+
/** Destination ids the service last saw refusing this machine, sorted. */
|
|
18
|
+
export async function unreachableDestinations(dataDir) {
|
|
19
|
+
try {
|
|
20
|
+
const raw = JSON.parse(await readFile(reachabilityPath(dataDir), "utf8"));
|
|
21
|
+
return Object.entries(raw).filter(([, reachable]) => reachable === false).map(([id]) => id).sort();
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// No observation yet, or an unreadable file. Saying nothing is correct:
|
|
25
|
+
// this is an advisory line after somebody's real answer, and a warning
|
|
26
|
+
// invented from a missing file would be worse than silence.
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Says what broke, what it means for the work, and what to do — in that order.
|
|
32
|
+
* "Nothing is lost" is the part a person needs first: the archives are still
|
|
33
|
+
* here and still sealed, they simply have not left this machine yet.
|
|
34
|
+
*/
|
|
35
|
+
export function unreachableNotice(names) {
|
|
36
|
+
const subject = names.length === 1 ? `${names[0]} is` : `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]} are`;
|
|
37
|
+
return `${subject} not accepting this machine. New archives stay here and nothing is lost; reconnect in Settings (\`sealkeep ui\`).`;
|
|
38
|
+
}
|
package/package.json
CHANGED
package/web/app.js
CHANGED
|
@@ -4491,6 +4491,7 @@ function renderSettingsForm(local, _resolvedTrashStrategy) {
|
|
|
4491
4491
|
const check = (id, on) => { const el = $(id); if (el) el.checked = on; };
|
|
4492
4492
|
const fill = (id, value) => { const el = $(id); if (el) el.value = value; };
|
|
4493
4493
|
check("f-auto-inject", local.recall?.autoInject !== false);
|
|
4494
|
+
check("f-at-compaction", local.recall?.atCompaction !== false);
|
|
4494
4495
|
check("f-mcp-enabled", local.mcp?.enabled !== false);
|
|
4495
4496
|
fill("f-cpu", local.limits?.cpuPercent ?? "");
|
|
4496
4497
|
fill("f-memory", local.limits?.memoryMb ?? "");
|
|
@@ -4521,7 +4522,7 @@ function collectSettingsPatch() {
|
|
|
4521
4522
|
const raw = $("f-reserve").value.trim();
|
|
4522
4523
|
return raw === "" ? null : Math.min(102_400, Math.max(200, Math.round(Number(raw) || 0)));
|
|
4523
4524
|
})(),
|
|
4524
|
-
recall: { autoInject: $("f-auto-inject")?.checked !== false },
|
|
4525
|
+
recall: { autoInject: $("f-auto-inject")?.checked !== false, atCompaction: $("f-at-compaction")?.checked !== false },
|
|
4525
4526
|
mcp: { enabled: $("f-mcp-enabled")?.checked !== false },
|
|
4526
4527
|
limits: {
|
|
4527
4528
|
cpuPercent: (() => {
|
package/web/index.html
CHANGED
|
@@ -329,6 +329,11 @@
|
|
|
329
329
|
<p class="note">On by default. Your agent opens a project and already knows where you left off, without being asked.</p>
|
|
330
330
|
<p class="note"><b>What it costs.</b> Anything placed in a conversation is re-read by the model on every later step, so a block added at the start is paid for again on every request that follows. Sealkeep sends each thing once and never repeats itself, which on a measured session cut this from 86 million tokens to 11 million, under one percent of what that session sent. Turn this off and preserved history is still written as notes your agent reads for itself, and is still searchable — it simply is not volunteered.</p>
|
|
331
331
|
</div>
|
|
332
|
+
<div class="field">
|
|
333
|
+
<label class="opt"><input id="f-at-compaction" type="checkbox"> Give the agent its bearings back after its conversation is compacted</label>
|
|
334
|
+
<p class="note">On by default. When an agent's conversation is replaced by a summary, Sealkeep hands it the short version of where the work stands, once. Without that, an agent typically re-reads files to work out where it was, which costs far more than the note does.</p>
|
|
335
|
+
<p class="note"><b>Why it saves money.</b> The whole conversation is re-sent to the model on every step, so cost is roughly the size of that conversation times the number of steps. Compacting is the only thing that makes a running session cheaper, and this is what makes compacting early safe: nothing is lost either way, because the full transcript is preserved and searchable.</p>
|
|
336
|
+
</div>
|
|
332
337
|
<div class="field">
|
|
333
338
|
<label class="opt"><input id="f-mcp-enabled" type="checkbox"> Let agents search this vault themselves</label>
|
|
334
339
|
<p class="note">On by default. Runs a local tool server so Claude Code and Codex can search your sealed history on demand. Nothing is exposed to the network. Turn it off and agents keep the notes and the automatic recall, but cannot go looking for more.</p>
|