switchroom 0.19.39 → 0.19.40
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/agent-scheduler/index.js +10 -1
- package/dist/auth-broker/index.js +12 -3
- package/dist/cli/notion-write-pretool.mjs +10 -1
- package/dist/cli/switchroom.js +524 -189
- package/dist/host-control/main.js +217 -5
- package/dist/vault/approvals/kernel-server.js +12 -3
- package/dist/vault/broker/server.js +12 -3
- package/package.json +1 -1
- package/profiles/_base/start.sh.hbs +10 -0
- package/telegram-plugin/dist/bridge/bridge.js +8 -0
- package/telegram-plugin/dist/gateway/gateway.js +314 -191
- package/telegram-plugin/dist/server.js +8 -0
- package/telegram-plugin/gateway/backstop-delivery.ts +48 -0
- package/telegram-plugin/gateway/compaction-marker.ts +84 -0
- package/telegram-plugin/gateway/gateway.ts +3 -3
- package/telegram-plugin/gateway/liveness-wiring.ts +15 -0
- package/telegram-plugin/gateway/outbound-send-path.ts +20 -0
- package/telegram-plugin/gateway/outbox-sweep.ts +116 -18
- package/telegram-plugin/gateway/silence-poke-session-event.ts +13 -0
- package/telegram-plugin/gateway/stream-render.ts +39 -1
- package/telegram-plugin/hooks/compaction-marker-precompact.mjs +70 -0
- package/telegram-plugin/hooks/hooks.json +11 -0
- package/telegram-plugin/session-tail.ts +20 -0
- package/telegram-plugin/silence-poke.ts +28 -0
- package/telegram-plugin/tests/outbox-delivery.test.ts +38 -1
- package/telegram-plugin/tests/outbox-flush-ack-claim-race.test.ts +213 -0
- package/telegram-plugin/tests/outbox-reply-then-recap-e2e.test.ts +1 -1
- package/telegram-plugin/tests/outbox-sweep-flood-breaker.test.ts +4 -4
- package/telegram-plugin/tests/outbox-sweep-listen-button.test.ts +71 -8
- package/telegram-plugin/tests/send-reply-golden.test.ts +47 -0
- package/telegram-plugin/tests/silence-poke-compaction.test.ts +222 -0
package/dist/cli/switchroom.js
CHANGED
|
@@ -2120,7 +2120,7 @@ var init_esm = __esm(() => {
|
|
|
2120
2120
|
});
|
|
2121
2121
|
|
|
2122
2122
|
// src/build-info.ts
|
|
2123
|
-
var VERSION = "0.19.
|
|
2123
|
+
var VERSION = "0.19.40", COMMIT_SHA = "89286b22";
|
|
2124
2124
|
|
|
2125
2125
|
// src/cli/resolve-version.ts
|
|
2126
2126
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -13593,7 +13593,7 @@ function resolveObservationScope(configured, env2 = process.env) {
|
|
|
13593
13593
|
return classifyObservationScope(configured);
|
|
13594
13594
|
return classifyObservationScope(env2[OBSERVATION_SCOPES_ENV]);
|
|
13595
13595
|
}
|
|
13596
|
-
var OBSERVATION_SCOPES, OBSERVATION_SCOPES_ENV = "HINDSIGHT_OBSERVATION_SCOPES";
|
|
13596
|
+
var OBSERVATION_SCOPES, OBSERVATION_SCOPES_ENV = "HINDSIGHT_OBSERVATION_SCOPES", OBSERVATION_SCOPE_STRATEGIES;
|
|
13597
13597
|
var init_observation_scopes = __esm(() => {
|
|
13598
13598
|
OBSERVATION_SCOPES = [
|
|
13599
13599
|
"per_tag",
|
|
@@ -13601,13 +13601,19 @@ var init_observation_scopes = __esm(() => {
|
|
|
13601
13601
|
"all_combinations",
|
|
13602
13602
|
"shared"
|
|
13603
13603
|
];
|
|
13604
|
+
OBSERVATION_SCOPE_STRATEGIES = [
|
|
13605
|
+
"curated",
|
|
13606
|
+
"shared",
|
|
13607
|
+
"combined",
|
|
13608
|
+
"off"
|
|
13609
|
+
];
|
|
13604
13610
|
});
|
|
13605
13611
|
|
|
13606
13612
|
// src/config/schema.ts
|
|
13607
13613
|
function isValidTimezone(value) {
|
|
13608
13614
|
return TIMEZONE_REGEX.test(value);
|
|
13609
13615
|
}
|
|
13610
|
-
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, MicrosoftAccountEmailSchema, MicrosoftToolTokenSchema, MicrosoftAccountBindingSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, releaseBlockFields, ReleaseBlock, RootReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, DEFAULT_PROFILE = "default", AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
|
|
13616
|
+
var CodeRepoEntrySchema, AgentBindMountSchema, HttpDiffPollSchema, PollSpecSchema, TelegramMessageActionSchema, WebhookActionSchema, ActionSpecSchema, ScheduleEntrySchema, AgentSoulSchema, AgentToolsSchema, ObservationScopesSchema, ObservationScopeStrategySchema, AgentMemorySchema, HookEntrySchema, AgentHooksSchema, SubagentSchema, SessionSchema, SessionContinuitySchema, webhookDispatchRule, TelegramChannelSchema, ChannelsSchema, TIMEZONE_REGEX, ApproverIdSchema, GoogleWorkspaceTierSchema, GoogleWorkspaceConfigSchema, LiteLLMConfigSchema, HindsightPerOpLlmSchema, HindsightConfigSchema, MicrosoftWorkspaceConfigSchema, NotionWorkspaceConfigSchema, AgentGoogleWorkspaceConfigSchema, MicrosoftAccountEmailSchema, MicrosoftToolTokenSchema, MicrosoftAccountBindingSchema, AgentMicrosoftWorkspaceConfigSchema, AgentNotionWorkspaceConfigSchema, ReactionsSchema, ReactionDispatchSchema, releaseBlockFields, ReleaseBlock, RootReleaseBlock, NetworkIsolationSchema, servesField, knowsField, profileFields, ProfileSchema, _omitExtends, defaultsFields, AgentDefaultsSchema, DEFAULT_PROFILE = "default", AgentSchema, TelegramConfigSchema, MemoryBackendConfigSchema, VaultConfigSchema, QuotaConfigSchema, AutoReleaseCheckSchema, HostControlConfigSchema, WebServiceConfigSchema, FleetHealthConfigSchema, HostdConfigSchema, CronEgressSchema, CronConfigSchema, UserSchema, SwitchroomConfigSchema;
|
|
13611
13617
|
var init_schema = __esm(() => {
|
|
13612
13618
|
init_zod();
|
|
13613
13619
|
init_observation_scopes();
|
|
@@ -13722,7 +13728,8 @@ var init_schema = __esm(() => {
|
|
|
13722
13728
|
allow: exports_external.array(exports_external.string()).default([]).describe("Allowed tools (use ['all'] for unrestricted)"),
|
|
13723
13729
|
deny: exports_external.array(exports_external.string()).default([]).describe("Denied tools (overrides allow)")
|
|
13724
13730
|
}).optional();
|
|
13725
|
-
ObservationScopesSchema = exports_external.enum(OBSERVATION_SCOPES).optional().describe("
|
|
13731
|
+
ObservationScopesSchema = exports_external.enum(OBSERVATION_SCOPES).optional().describe("Operator PIN for the per-row observation scope, overriding the " + 'default `curated` strategy (see docs/configuration.md). "shared" ' + "forces Hindsight's consolidation to write EVERY observation into ONE " + "global untagged scope \u2014 what several agents pooling one bank want so " + "their observations merge rather than sitting in parallel silos. " + '"combined" is the pin form of opting out (restores the pre-#4035 ' + "engine default). UNSET does NOT mean 'nothing on the wire': the " + "`curated` strategy still computes a scope per retain. Applies to " + "every retain path (Stop hook, " + "sidechain, boot reconcile, queue drain, backfill, session-handoff " + "mirror) and is carried on the queued payload, so a retain that fails " + "now and drains later still lands in this scope. Accepted values: " + `${OBSERVATION_SCOPES.join(", ")}. ` + "Cascade: override (per-agent wins over default).");
|
|
13732
|
+
ObservationScopeStrategySchema = exports_external.enum(OBSERVATION_SCOPE_STRATEGIES).optional().describe("Per-retain strategy that decides how the observation scope is " + 'computed when no `observation_scopes` pin is set. "curated" ' + "(the default since #4035) strips volatile per-session provenance " + "tags from each retain's consolidation scope \u2014 keeping the stable " + "semantic ones on the source fact \u2014 so an agent's observations dedup " + "and merge across sessions instead of pocketing one-per-session. " + '"shared" sends every retain to the one global untagged scope. ' + '"combined" / "off" opt OUT: no per-row scope goes on the wire, ' + "restoring the pre-feature engine default (byte-identical to an " + "unconfigured client). OMITTED BY DEFAULT: unset leaves the plugin's " + "own default (`curated`) in force. A manual `observation_scopes` pin " + "wins over this strategy. Accepted values: " + `${OBSERVATION_SCOPE_STRATEGIES.join(", ")}. ` + "Cascade: override (per-agent wins over default).");
|
|
13726
13733
|
AgentMemorySchema = exports_external.object({
|
|
13727
13734
|
collection: exports_external.string().describe("Hindsight collection name for this agent"),
|
|
13728
13735
|
auto_recall: exports_external.boolean().default(true).describe("Auto-search memories before each response"),
|
|
@@ -13758,6 +13765,7 @@ var init_schema = __esm(() => {
|
|
|
13758
13765
|
}).optional().describe("Personality traits (1-5 each) steering how this bank frames recall, " + "reflect, and observation synthesis \u2014 a coach leans empathy-high, a " + "lawyer/analyst leans skepticism/literalism-high. Maps to the engine's " + "flat `disposition_skepticism`/`_literalism`/`_empathy` fields. " + "Cascade: per-key merge (an agent overrides individual traits and " + "inherits the rest, matching `recall`)."),
|
|
13759
13766
|
directive_capture_nudge: exports_external.boolean().optional().describe("Deterministic directive-capture nudge (issue #2848 Stage B). When " + "on (switchroom default true \u2014 Stage A measured a ~55% miss rate on " + "durable corrections), the auto-recall hook regex-detects correction " + '/ standing-rule-shaped inbound ("always/never \u2026", "from now on \u2026", ' + `"stop doing \u2026", a stated preference, "that's wrong, it's \u2026") and ` + "appends a terse advisory to the turn's context telling the model to " + "persist the rule with mcp__hindsight__create_directive if it IS " + "durable. Detection is pure regex \u2014 the model does the judgment " + "in-session and calls create_directive itself (no model callsite, no " + "silent hook-side write). Set false to disable per-agent. " + "Cascade: override (per-agent wins over default)."),
|
|
13760
13767
|
observation_scopes: ObservationScopesSchema,
|
|
13768
|
+
observation_scope_strategy: ObservationScopeStrategySchema,
|
|
13761
13769
|
recall: exports_external.object({
|
|
13762
13770
|
max_memories: exports_external.number().int().min(0).optional().describe("Cap on the number of memories injected into the prompt by " + "auto-recall, regardless of token budget. Plugin default is 12. " + "0 disables the cap (all memories Hindsight returns are injected)."),
|
|
13763
13771
|
cache_ttl_secs: exports_external.number().int().min(0).optional().describe("Per-session recall cache TTL in seconds. When > 0, identical " + "(prompt, bank) within the same session reuse the cached recall " + "result instead of round-tripping to Hindsight. 0 disables. " + "Default is 600 (10 min) for switchroom-managed agents."),
|
|
@@ -14158,6 +14166,7 @@ var init_schema = __esm(() => {
|
|
|
14158
14166
|
isolation: exports_external.enum(["default", "strict"]).optional(),
|
|
14159
14167
|
directive_capture_nudge: exports_external.boolean().optional(),
|
|
14160
14168
|
observation_scopes: ObservationScopesSchema,
|
|
14169
|
+
observation_scope_strategy: ObservationScopeStrategySchema,
|
|
14161
14170
|
recall: exports_external.object({
|
|
14162
14171
|
max_memories: exports_external.number().int().min(0).optional(),
|
|
14163
14172
|
cache_ttl_secs: exports_external.number().int().min(0).optional(),
|
|
@@ -29102,6 +29111,7 @@ function buildWorkspaceContext(args) {
|
|
|
29102
29111
|
hindsightRecallSkipTrivial,
|
|
29103
29112
|
hindsightDirectiveCaptureNudge,
|
|
29104
29113
|
hindsightObservationScopes,
|
|
29114
|
+
hindsightObservationScopeStrategy,
|
|
29105
29115
|
hindsightTopicAliasesJson,
|
|
29106
29116
|
hindsightSenderBanksJson,
|
|
29107
29117
|
hindsightTopicFilterMode
|
|
@@ -29156,6 +29166,7 @@ function buildWorkspaceContext(args) {
|
|
|
29156
29166
|
hindsightRecallSkipTrivial,
|
|
29157
29167
|
hindsightDirectiveCaptureNudge,
|
|
29158
29168
|
hindsightObservationScopesQ: hindsightObservationScopes ? shellSingleQuote(hindsightObservationScopes) : undefined,
|
|
29169
|
+
hindsightObservationScopeStrategyQ: hindsightObservationScopeStrategy ? shellSingleQuote(hindsightObservationScopeStrategy) : undefined,
|
|
29159
29170
|
hindsightTopicAliasesJsonQ: hindsightTopicAliasesJson ? shellSingleQuote(hindsightTopicAliasesJson) : undefined,
|
|
29160
29171
|
hindsightSenderBanksJsonQ: hindsightSenderBanksJson ? shellSingleQuote(hindsightSenderBanksJson) : undefined,
|
|
29161
29172
|
hindsightTopicFilterMode,
|
|
@@ -29421,6 +29432,7 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
|
|
|
29421
29432
|
const hindsightRecallSkipTrivial = agentConfig.memory?.recall?.skip_trivial === undefined ? undefined : String(agentConfig.memory.recall.skip_trivial);
|
|
29422
29433
|
const hindsightDirectiveCaptureNudge = agentConfig.memory?.directive_capture_nudge === undefined ? undefined : String(agentConfig.memory.directive_capture_nudge);
|
|
29423
29434
|
const hindsightObservationScopes = agentConfig.memory?.observation_scopes;
|
|
29435
|
+
const hindsightObservationScopeStrategy = agentConfig.memory?.observation_scope_strategy;
|
|
29424
29436
|
const topicAliases = agentConfig.channels?.telegram?.topic_aliases;
|
|
29425
29437
|
const hindsightTopicAliasesJson = topicAliases && Object.keys(topicAliases).length > 0 ? JSON.stringify(topicAliases) : undefined;
|
|
29426
29438
|
const senderBanks = switchroomConfig ? resolveUsers(switchroomConfig, name).senderBanks : agentConfig.memory?.recall?.sender_banks ?? {};
|
|
@@ -29457,6 +29469,7 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
|
|
|
29457
29469
|
hindsightRecallSkipTrivial,
|
|
29458
29470
|
hindsightDirectiveCaptureNudge,
|
|
29459
29471
|
hindsightObservationScopes,
|
|
29472
|
+
hindsightObservationScopeStrategy,
|
|
29460
29473
|
hindsightTopicAliasesJson,
|
|
29461
29474
|
hindsightSenderBanksJson,
|
|
29462
29475
|
hindsightTopicFilterMode
|
|
@@ -30402,6 +30415,7 @@ function reconcileAgentInner(name, agentConfigRaw, agentsDir, telegramConfig, sw
|
|
|
30402
30415
|
const hindsightRecallSkipTrivial = agentConfig.memory?.recall?.skip_trivial === undefined ? undefined : String(agentConfig.memory.recall.skip_trivial);
|
|
30403
30416
|
const hindsightDirectiveCaptureNudge = agentConfig.memory?.directive_capture_nudge === undefined ? undefined : String(agentConfig.memory.directive_capture_nudge);
|
|
30404
30417
|
const hindsightObservationScopes = agentConfig.memory?.observation_scopes;
|
|
30418
|
+
const hindsightObservationScopeStrategy = agentConfig.memory?.observation_scope_strategy;
|
|
30405
30419
|
const topicAliases = agentConfig.channels?.telegram?.topic_aliases;
|
|
30406
30420
|
const hindsightTopicAliasesJson = topicAliases && Object.keys(topicAliases).length > 0 ? JSON.stringify(topicAliases) : undefined;
|
|
30407
30421
|
const senderBanks = switchroomConfig ? resolveUsers(switchroomConfig, name).senderBanks : agentConfig.memory?.recall?.sender_banks ?? {};
|
|
@@ -30451,6 +30465,7 @@ function reconcileAgentInner(name, agentConfigRaw, agentsDir, telegramConfig, sw
|
|
|
30451
30465
|
hindsightRecallSkipTrivial,
|
|
30452
30466
|
hindsightDirectiveCaptureNudge,
|
|
30453
30467
|
hindsightObservationScopesQ: hindsightObservationScopes ? shellSingleQuote(hindsightObservationScopes) : undefined,
|
|
30468
|
+
hindsightObservationScopeStrategyQ: hindsightObservationScopeStrategy ? shellSingleQuote(hindsightObservationScopeStrategy) : undefined,
|
|
30454
30469
|
hindsightTopicAliasesJsonQ: hindsightTopicAliasesJson ? shellSingleQuote(hindsightTopicAliasesJson) : undefined,
|
|
30455
30470
|
hindsightSenderBanksJsonQ: hindsightSenderBanksJson ? shellSingleQuote(hindsightSenderBanksJson) : undefined,
|
|
30456
30471
|
hindsightTopicFilterMode,
|
|
@@ -30874,6 +30889,7 @@ ${body}
|
|
|
30874
30889
|
hindsightRecallSkipTrivial,
|
|
30875
30890
|
hindsightDirectiveCaptureNudge,
|
|
30876
30891
|
hindsightObservationScopes,
|
|
30892
|
+
hindsightObservationScopeStrategy,
|
|
30877
30893
|
hindsightTopicAliasesJson,
|
|
30878
30894
|
hindsightSenderBanksJson,
|
|
30879
30895
|
hindsightTopicFilterMode
|
|
@@ -39115,168 +39131,6 @@ var init_telegram_yaml = __esm(() => {
|
|
|
39115
39131
|
import_yaml12 = __toESM(require_dist(), 1);
|
|
39116
39132
|
});
|
|
39117
39133
|
|
|
39118
|
-
// src/memory/hindsight-tools.ts
|
|
39119
|
-
var HINDSIGHT_MIN_API_VERSION = "0.8.5", HINDSIGHT_REPAIR_MIN_API_VERSION = "0.8.5", EXPECTED_HINDSIGHT_TOOLS;
|
|
39120
|
-
var init_hindsight_tools = __esm(() => {
|
|
39121
|
-
EXPECTED_HINDSIGHT_TOOLS = {
|
|
39122
|
-
recall: { required: ["query"] },
|
|
39123
|
-
reflect: { required: ["query"] },
|
|
39124
|
-
retain: { required: ["content"] },
|
|
39125
|
-
sync_retain: { required: ["content"] },
|
|
39126
|
-
delete_document: { required: ["document_id"] },
|
|
39127
|
-
create_directive: { required: ["content", "name"] },
|
|
39128
|
-
list_directives: { required: [] },
|
|
39129
|
-
delete_directive: { required: ["directive_id"] },
|
|
39130
|
-
create_bank: { required: ["bank_id"] },
|
|
39131
|
-
update_bank: { required: [] },
|
|
39132
|
-
list_banks: { required: [] },
|
|
39133
|
-
create_mental_model: { required: ["name", "source_query"] },
|
|
39134
|
-
list_mental_models: { required: [] },
|
|
39135
|
-
update_mental_model: { required: ["mental_model_id"] },
|
|
39136
|
-
refresh_mental_model: { required: ["mental_model_id"] },
|
|
39137
|
-
list_memories: { required: [] },
|
|
39138
|
-
get_memory: { required: ["memory_id"] },
|
|
39139
|
-
update_memory: { required: ["memory_id"] },
|
|
39140
|
-
invalidate_memory: { required: ["memory_id"] }
|
|
39141
|
-
};
|
|
39142
|
-
});
|
|
39143
|
-
|
|
39144
|
-
// src/memory/hindsight-repair.ts
|
|
39145
|
-
function buildRepairBankArgv(opts) {
|
|
39146
|
-
const hasBank = typeof opts.bank === "string" && opts.bank.length > 0;
|
|
39147
|
-
if (hasBank === Boolean(opts.all)) {
|
|
39148
|
-
throw new Error("pass exactly one of --bank <id> or --all");
|
|
39149
|
-
}
|
|
39150
|
-
const cmdArgs = ["repair-bank"];
|
|
39151
|
-
if (hasBank)
|
|
39152
|
-
cmdArgs.push("--bank", opts.bank);
|
|
39153
|
-
if (opts.all)
|
|
39154
|
-
cmdArgs.push("--all");
|
|
39155
|
-
if (opts.schema)
|
|
39156
|
-
cmdArgs.push("--schema", opts.schema);
|
|
39157
|
-
if (opts.dryRun)
|
|
39158
|
-
cmdArgs.push("--dry-run");
|
|
39159
|
-
const script = `if command -v hindsight-admin >/dev/null 2>&1; then exec hindsight-admin "$@"; fi; ` + `exec ${HINDSIGHT_ADMIN_BIN} "$@"`;
|
|
39160
|
-
return [
|
|
39161
|
-
"exec",
|
|
39162
|
-
opts.container ?? HINDSIGHT_CONTAINER_NAME,
|
|
39163
|
-
"sh",
|
|
39164
|
-
"-c",
|
|
39165
|
-
script,
|
|
39166
|
-
"--",
|
|
39167
|
-
...cmdArgs
|
|
39168
|
-
];
|
|
39169
|
-
}
|
|
39170
|
-
function parseRepairSummary(stdout) {
|
|
39171
|
-
const m = stdout.match(/Done:\s*(\d+)\s+schema\(s\),\s*(\d+)\s+bank\(s\) scanned,\s*(\d+)\s+already present,\s*(\d+)\s+created,\s*(\d+)\s+to-create \(dry-run\),\s*(\d+)\s+failed/);
|
|
39172
|
-
if (!m)
|
|
39173
|
-
return null;
|
|
39174
|
-
return {
|
|
39175
|
-
schemas: Number(m[1]),
|
|
39176
|
-
banksScanned: Number(m[2]),
|
|
39177
|
-
alreadyPresent: Number(m[3]),
|
|
39178
|
-
created: Number(m[4]),
|
|
39179
|
-
toCreate: Number(m[5]),
|
|
39180
|
-
failed: Number(m[6])
|
|
39181
|
-
};
|
|
39182
|
-
}
|
|
39183
|
-
function compareApiVersion(a, b) {
|
|
39184
|
-
const part = (v, i) => {
|
|
39185
|
-
const raw = v.split(".")[i];
|
|
39186
|
-
if (raw === undefined)
|
|
39187
|
-
return 0;
|
|
39188
|
-
const n = parseInt(raw, 10);
|
|
39189
|
-
return Number.isFinite(n) ? n : 0;
|
|
39190
|
-
};
|
|
39191
|
-
const len = Math.max(a.split(".").length, b.split(".").length);
|
|
39192
|
-
for (let i = 0;i < len; i++) {
|
|
39193
|
-
const d = part(a, i) - part(b, i);
|
|
39194
|
-
if (d !== 0)
|
|
39195
|
-
return d;
|
|
39196
|
-
}
|
|
39197
|
-
return 0;
|
|
39198
|
-
}
|
|
39199
|
-
async function fetchHindsightApiVersion(mcpUrl, opts = {}) {
|
|
39200
|
-
const doFetch = opts.fetchImpl ?? fetch;
|
|
39201
|
-
const origin = mcpUrl.replace(/\/mcp\/?$/, "").replace(/\/$/, "");
|
|
39202
|
-
const controller = new AbortController;
|
|
39203
|
-
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 4000);
|
|
39204
|
-
try {
|
|
39205
|
-
const res = await doFetch(`${origin}/version`, { signal: controller.signal });
|
|
39206
|
-
if (!res.ok)
|
|
39207
|
-
return null;
|
|
39208
|
-
const body = await res.json();
|
|
39209
|
-
return typeof body?.api_version === "string" ? body.api_version : null;
|
|
39210
|
-
} catch {
|
|
39211
|
-
return null;
|
|
39212
|
-
} finally {
|
|
39213
|
-
clearTimeout(timer);
|
|
39214
|
-
}
|
|
39215
|
-
}
|
|
39216
|
-
function classifyRepairPreflight(liveVersion) {
|
|
39217
|
-
if (liveVersion === null)
|
|
39218
|
-
return { ok: true };
|
|
39219
|
-
if (compareApiVersion(liveVersion, HINDSIGHT_REPAIR_MIN_API_VERSION) < 0) {
|
|
39220
|
-
return {
|
|
39221
|
-
ok: false,
|
|
39222
|
-
reason: `hindsight ${liveVersion} has no \`repair-bank\` command \u2014 it lands in ` + `${HINDSIGHT_REPAIR_MIN_API_VERSION} (upstream #2645). Without it there is no ` + `supported way to rebuild a bank's vector-index coverage. Update the ` + `pinned image in docker/Dockerfile.hindsight and recreate the container ` + `with \`switchroom memory --update\`, then re-run this command.`
|
|
39223
|
-
};
|
|
39224
|
-
}
|
|
39225
|
-
return { ok: true };
|
|
39226
|
-
}
|
|
39227
|
-
function classifyRepairOutcome(run) {
|
|
39228
|
-
if (run.childExit !== 0) {
|
|
39229
|
-
return {
|
|
39230
|
-
exit: REPAIR_FAILED_EXIT,
|
|
39231
|
-
level: "fail",
|
|
39232
|
-
headline: `repair-bank exited ${run.childExit}.`,
|
|
39233
|
-
detail: run.childExit === 2 ? "Exit 2 from the admin CLI is a usage error \u2014 check --bank / --schema spelling. " + "Nothing was repaired and coverage is unknown." : "Failed indexes are dropped, so a re-run is safe and is the documented retry."
|
|
39234
|
-
};
|
|
39235
|
-
}
|
|
39236
|
-
if (run.summary === null) {
|
|
39237
|
-
return {
|
|
39238
|
-
exit: REPAIR_UNVERIFIED_EXIT,
|
|
39239
|
-
level: "fail",
|
|
39240
|
-
headline: "repair-bank exited 0 but printed no summary \u2014 coverage was NOT confirmed.",
|
|
39241
|
-
detail: "Read its output above. This is not a pass: a backend with a single global " + "index, an aborted scan, or an output-format change upstream all look like " + "this, and none of them means coverage is complete."
|
|
39242
|
-
};
|
|
39243
|
-
}
|
|
39244
|
-
if (run.summary.banksScanned === 0) {
|
|
39245
|
-
return {
|
|
39246
|
-
exit: REPAIR_UNVERIFIED_EXIT,
|
|
39247
|
-
level: "fail",
|
|
39248
|
-
headline: "repair-bank scanned 0 bank(s) \u2014 nothing was checked.",
|
|
39249
|
-
detail: "A scan that examined no bank proves nothing about coverage. Usual cause is a " + "--bank id or --schema name that matches nothing; `switchroom memory banks` " + "lists the real ids."
|
|
39250
|
-
};
|
|
39251
|
-
}
|
|
39252
|
-
if (run.summary.failed > 0) {
|
|
39253
|
-
return {
|
|
39254
|
-
exit: REPAIR_FAILED_EXIT,
|
|
39255
|
-
level: "fail",
|
|
39256
|
-
headline: `${run.summary.failed} index(es) failed to build.`,
|
|
39257
|
-
detail: "Those banks still under-return on recall. Failed indexes are dropped, so " + "re-running is safe and is the documented retry; if it repeats, the hindsight " + "container logs carry the postgres error."
|
|
39258
|
-
};
|
|
39259
|
-
}
|
|
39260
|
-
if (run.dryRun && run.summary.toCreate > 0) {
|
|
39261
|
-
return {
|
|
39262
|
-
exit: REPAIR_COVERAGE_MISSING_EXIT,
|
|
39263
|
-
level: "fail",
|
|
39264
|
-
headline: `${run.summary.toCreate} vector index(es) missing across ${run.summary.banksScanned} bank(s).`,
|
|
39265
|
-
detail: "Recall on those banks silently under-returns. Re-run without --dry-run to fix."
|
|
39266
|
-
};
|
|
39267
|
-
}
|
|
39268
|
-
return {
|
|
39269
|
-
exit: 0,
|
|
39270
|
-
level: "ok",
|
|
39271
|
-
headline: "repair-bank finished.",
|
|
39272
|
-
detail: run.dryRun ? `Coverage complete: ${run.summary.alreadyPresent} index(es) present across ` + `${run.summary.banksScanned} bank(s), nothing to create.` : `${run.summary.created} created, ${run.summary.alreadyPresent} already present ` + `across ${run.summary.banksScanned} bank(s). Re-run \`switchroom doctor\` to confirm.`
|
|
39273
|
-
};
|
|
39274
|
-
}
|
|
39275
|
-
var HINDSIGHT_CONTAINER_NAME = "switchroom-hindsight", HINDSIGHT_ADMIN_BIN = "/app/api/.venv/bin/hindsight-admin", REPAIR_FAILED_EXIT = 1, REPAIR_COVERAGE_MISSING_EXIT = 3, REPAIR_UNVERIFIED_EXIT = 4;
|
|
39276
|
-
var init_hindsight_repair = __esm(() => {
|
|
39277
|
-
init_hindsight_tools();
|
|
39278
|
-
});
|
|
39279
|
-
|
|
39280
39134
|
// src/memory/bank-health.ts
|
|
39281
39135
|
function hindsightRestBase(mcpUrl) {
|
|
39282
39136
|
return mcpUrl.replace(/\/mcp\/?$/, "").replace(/\/$/, "");
|
|
@@ -39446,6 +39300,168 @@ function recentUnextracted(docs, withinDays = 30, now = new Date, minTextLength
|
|
|
39446
39300
|
}
|
|
39447
39301
|
var DOCUMENTS_PAGE_LIMIT = 500;
|
|
39448
39302
|
|
|
39303
|
+
// src/memory/hindsight-tools.ts
|
|
39304
|
+
var HINDSIGHT_MIN_API_VERSION = "0.8.5", HINDSIGHT_REPAIR_MIN_API_VERSION = "0.8.5", EXPECTED_HINDSIGHT_TOOLS;
|
|
39305
|
+
var init_hindsight_tools = __esm(() => {
|
|
39306
|
+
EXPECTED_HINDSIGHT_TOOLS = {
|
|
39307
|
+
recall: { required: ["query"] },
|
|
39308
|
+
reflect: { required: ["query"] },
|
|
39309
|
+
retain: { required: ["content"] },
|
|
39310
|
+
sync_retain: { required: ["content"] },
|
|
39311
|
+
delete_document: { required: ["document_id"] },
|
|
39312
|
+
create_directive: { required: ["content", "name"] },
|
|
39313
|
+
list_directives: { required: [] },
|
|
39314
|
+
delete_directive: { required: ["directive_id"] },
|
|
39315
|
+
create_bank: { required: ["bank_id"] },
|
|
39316
|
+
update_bank: { required: [] },
|
|
39317
|
+
list_banks: { required: [] },
|
|
39318
|
+
create_mental_model: { required: ["name", "source_query"] },
|
|
39319
|
+
list_mental_models: { required: [] },
|
|
39320
|
+
update_mental_model: { required: ["mental_model_id"] },
|
|
39321
|
+
refresh_mental_model: { required: ["mental_model_id"] },
|
|
39322
|
+
list_memories: { required: [] },
|
|
39323
|
+
get_memory: { required: ["memory_id"] },
|
|
39324
|
+
update_memory: { required: ["memory_id"] },
|
|
39325
|
+
invalidate_memory: { required: ["memory_id"] }
|
|
39326
|
+
};
|
|
39327
|
+
});
|
|
39328
|
+
|
|
39329
|
+
// src/memory/hindsight-repair.ts
|
|
39330
|
+
function buildRepairBankArgv(opts) {
|
|
39331
|
+
const hasBank = typeof opts.bank === "string" && opts.bank.length > 0;
|
|
39332
|
+
if (hasBank === Boolean(opts.all)) {
|
|
39333
|
+
throw new Error("pass exactly one of --bank <id> or --all");
|
|
39334
|
+
}
|
|
39335
|
+
const cmdArgs = ["repair-bank"];
|
|
39336
|
+
if (hasBank)
|
|
39337
|
+
cmdArgs.push("--bank", opts.bank);
|
|
39338
|
+
if (opts.all)
|
|
39339
|
+
cmdArgs.push("--all");
|
|
39340
|
+
if (opts.schema)
|
|
39341
|
+
cmdArgs.push("--schema", opts.schema);
|
|
39342
|
+
if (opts.dryRun)
|
|
39343
|
+
cmdArgs.push("--dry-run");
|
|
39344
|
+
const script = `if command -v hindsight-admin >/dev/null 2>&1; then exec hindsight-admin "$@"; fi; ` + `exec ${HINDSIGHT_ADMIN_BIN} "$@"`;
|
|
39345
|
+
return [
|
|
39346
|
+
"exec",
|
|
39347
|
+
opts.container ?? HINDSIGHT_CONTAINER_NAME,
|
|
39348
|
+
"sh",
|
|
39349
|
+
"-c",
|
|
39350
|
+
script,
|
|
39351
|
+
"--",
|
|
39352
|
+
...cmdArgs
|
|
39353
|
+
];
|
|
39354
|
+
}
|
|
39355
|
+
function parseRepairSummary(stdout) {
|
|
39356
|
+
const m = stdout.match(/Done:\s*(\d+)\s+schema\(s\),\s*(\d+)\s+bank\(s\) scanned,\s*(\d+)\s+already present,\s*(\d+)\s+created,\s*(\d+)\s+to-create \(dry-run\),\s*(\d+)\s+failed/);
|
|
39357
|
+
if (!m)
|
|
39358
|
+
return null;
|
|
39359
|
+
return {
|
|
39360
|
+
schemas: Number(m[1]),
|
|
39361
|
+
banksScanned: Number(m[2]),
|
|
39362
|
+
alreadyPresent: Number(m[3]),
|
|
39363
|
+
created: Number(m[4]),
|
|
39364
|
+
toCreate: Number(m[5]),
|
|
39365
|
+
failed: Number(m[6])
|
|
39366
|
+
};
|
|
39367
|
+
}
|
|
39368
|
+
function compareApiVersion(a, b) {
|
|
39369
|
+
const part = (v, i) => {
|
|
39370
|
+
const raw = v.split(".")[i];
|
|
39371
|
+
if (raw === undefined)
|
|
39372
|
+
return 0;
|
|
39373
|
+
const n = parseInt(raw, 10);
|
|
39374
|
+
return Number.isFinite(n) ? n : 0;
|
|
39375
|
+
};
|
|
39376
|
+
const len = Math.max(a.split(".").length, b.split(".").length);
|
|
39377
|
+
for (let i = 0;i < len; i++) {
|
|
39378
|
+
const d = part(a, i) - part(b, i);
|
|
39379
|
+
if (d !== 0)
|
|
39380
|
+
return d;
|
|
39381
|
+
}
|
|
39382
|
+
return 0;
|
|
39383
|
+
}
|
|
39384
|
+
async function fetchHindsightApiVersion(mcpUrl, opts = {}) {
|
|
39385
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
39386
|
+
const origin = mcpUrl.replace(/\/mcp\/?$/, "").replace(/\/$/, "");
|
|
39387
|
+
const controller = new AbortController;
|
|
39388
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 4000);
|
|
39389
|
+
try {
|
|
39390
|
+
const res = await doFetch(`${origin}/version`, { signal: controller.signal });
|
|
39391
|
+
if (!res.ok)
|
|
39392
|
+
return null;
|
|
39393
|
+
const body = await res.json();
|
|
39394
|
+
return typeof body?.api_version === "string" ? body.api_version : null;
|
|
39395
|
+
} catch {
|
|
39396
|
+
return null;
|
|
39397
|
+
} finally {
|
|
39398
|
+
clearTimeout(timer);
|
|
39399
|
+
}
|
|
39400
|
+
}
|
|
39401
|
+
function classifyRepairPreflight(liveVersion) {
|
|
39402
|
+
if (liveVersion === null)
|
|
39403
|
+
return { ok: true };
|
|
39404
|
+
if (compareApiVersion(liveVersion, HINDSIGHT_REPAIR_MIN_API_VERSION) < 0) {
|
|
39405
|
+
return {
|
|
39406
|
+
ok: false,
|
|
39407
|
+
reason: `hindsight ${liveVersion} has no \`repair-bank\` command \u2014 it lands in ` + `${HINDSIGHT_REPAIR_MIN_API_VERSION} (upstream #2645). Without it there is no ` + `supported way to rebuild a bank's vector-index coverage. Update the ` + `pinned image in docker/Dockerfile.hindsight and recreate the container ` + `with \`switchroom memory --update\`, then re-run this command.`
|
|
39408
|
+
};
|
|
39409
|
+
}
|
|
39410
|
+
return { ok: true };
|
|
39411
|
+
}
|
|
39412
|
+
function classifyRepairOutcome(run) {
|
|
39413
|
+
if (run.childExit !== 0) {
|
|
39414
|
+
return {
|
|
39415
|
+
exit: REPAIR_FAILED_EXIT,
|
|
39416
|
+
level: "fail",
|
|
39417
|
+
headline: `repair-bank exited ${run.childExit}.`,
|
|
39418
|
+
detail: run.childExit === 2 ? "Exit 2 from the admin CLI is a usage error \u2014 check --bank / --schema spelling. " + "Nothing was repaired and coverage is unknown." : "Failed indexes are dropped, so a re-run is safe and is the documented retry."
|
|
39419
|
+
};
|
|
39420
|
+
}
|
|
39421
|
+
if (run.summary === null) {
|
|
39422
|
+
return {
|
|
39423
|
+
exit: REPAIR_UNVERIFIED_EXIT,
|
|
39424
|
+
level: "fail",
|
|
39425
|
+
headline: "repair-bank exited 0 but printed no summary \u2014 coverage was NOT confirmed.",
|
|
39426
|
+
detail: "Read its output above. This is not a pass: a backend with a single global " + "index, an aborted scan, or an output-format change upstream all look like " + "this, and none of them means coverage is complete."
|
|
39427
|
+
};
|
|
39428
|
+
}
|
|
39429
|
+
if (run.summary.banksScanned === 0) {
|
|
39430
|
+
return {
|
|
39431
|
+
exit: REPAIR_UNVERIFIED_EXIT,
|
|
39432
|
+
level: "fail",
|
|
39433
|
+
headline: "repair-bank scanned 0 bank(s) \u2014 nothing was checked.",
|
|
39434
|
+
detail: "A scan that examined no bank proves nothing about coverage. Usual cause is a " + "--bank id or --schema name that matches nothing; `switchroom memory banks` " + "lists the real ids."
|
|
39435
|
+
};
|
|
39436
|
+
}
|
|
39437
|
+
if (run.summary.failed > 0) {
|
|
39438
|
+
return {
|
|
39439
|
+
exit: REPAIR_FAILED_EXIT,
|
|
39440
|
+
level: "fail",
|
|
39441
|
+
headline: `${run.summary.failed} index(es) failed to build.`,
|
|
39442
|
+
detail: "Those banks still under-return on recall. Failed indexes are dropped, so " + "re-running is safe and is the documented retry; if it repeats, the hindsight " + "container logs carry the postgres error."
|
|
39443
|
+
};
|
|
39444
|
+
}
|
|
39445
|
+
if (run.dryRun && run.summary.toCreate > 0) {
|
|
39446
|
+
return {
|
|
39447
|
+
exit: REPAIR_COVERAGE_MISSING_EXIT,
|
|
39448
|
+
level: "fail",
|
|
39449
|
+
headline: `${run.summary.toCreate} vector index(es) missing across ${run.summary.banksScanned} bank(s).`,
|
|
39450
|
+
detail: "Recall on those banks silently under-returns. Re-run without --dry-run to fix."
|
|
39451
|
+
};
|
|
39452
|
+
}
|
|
39453
|
+
return {
|
|
39454
|
+
exit: 0,
|
|
39455
|
+
level: "ok",
|
|
39456
|
+
headline: "repair-bank finished.",
|
|
39457
|
+
detail: run.dryRun ? `Coverage complete: ${run.summary.alreadyPresent} index(es) present across ` + `${run.summary.banksScanned} bank(s), nothing to create.` : `${run.summary.created} created, ${run.summary.alreadyPresent} already present ` + `across ${run.summary.banksScanned} bank(s). Re-run \`switchroom doctor\` to confirm.`
|
|
39458
|
+
};
|
|
39459
|
+
}
|
|
39460
|
+
var HINDSIGHT_CONTAINER_NAME = "switchroom-hindsight", HINDSIGHT_ADMIN_BIN = "/app/api/.venv/bin/hindsight-admin", REPAIR_FAILED_EXIT = 1, REPAIR_COVERAGE_MISSING_EXIT = 3, REPAIR_UNVERIFIED_EXIT = 4;
|
|
39461
|
+
var init_hindsight_repair = __esm(() => {
|
|
39462
|
+
init_hindsight_tools();
|
|
39463
|
+
});
|
|
39464
|
+
|
|
39449
39465
|
// src/host-control/audit-rotation-config.ts
|
|
39450
39466
|
var DEFAULT_HOSTD_AUDIT_MAX_BYTES, DEFAULT_HOSTD_AUDIT_MAX_FILES = 3;
|
|
39451
39467
|
var init_audit_rotation_config = __esm(() => {
|
|
@@ -45781,6 +45797,8 @@ function classifyObservationScopeSaturation(reports) {
|
|
|
45781
45797
|
}
|
|
45782
45798
|
const unreadable = reports.filter((r) => !r.ok);
|
|
45783
45799
|
const readable = reports.filter((r) => r.ok);
|
|
45800
|
+
const growing = readable.filter((r) => r.observationsEnabled && r.untaggedCount >= OBSERVATION_SCOPE_UNTAGGED_WARN_COUNT).sort((a, b) => b.untaggedCount - a.untaggedCount);
|
|
45801
|
+
const growthNote = growing.length > 0 ? `${growing.length} bank(s) with an untagged global scope over ` + `${OBSERVATION_SCOPE_UNTAGGED_WARN_COUNT.toLocaleString()} observations ` + `(uncapped curated sink, growth watch): ` + growing.slice(0, OBSERVATION_SCOPE_MAX_LISTED).map((r) => `${r.bankId} ${r.untaggedCount.toLocaleString()}`).join(", ") + (growing.length > OBSERVATION_SCOPE_MAX_LISTED ? `; +${growing.length - OBSERVATION_SCOPE_MAX_LISTED} more` : "") : "";
|
|
45784
45802
|
if (readable.length === 0) {
|
|
45785
45803
|
return {
|
|
45786
45804
|
name,
|
|
@@ -45803,7 +45821,11 @@ function classifyObservationScopeSaturation(reports) {
|
|
|
45803
45821
|
notes.push(`${disabled.length} bank(s) have observations disabled: ` + disabled.map((r) => r.bankId).join(", "));
|
|
45804
45822
|
}
|
|
45805
45823
|
const suffix = notes.length > 0 ? ` \u00b7 ${notes.join(" \u00b7 ")}` : "";
|
|
45824
|
+
const growthSuffix = growthNote ? ` \u00b7 ${growthNote}` : "";
|
|
45806
45825
|
if (judged.length === 0) {
|
|
45826
|
+
if (growing.length > 0) {
|
|
45827
|
+
return { name, status: "warn", detail: `${growthNote}${suffix}` };
|
|
45828
|
+
}
|
|
45807
45829
|
return { name, status: "skip", detail: `no bank could be judged${suffix}` };
|
|
45808
45830
|
}
|
|
45809
45831
|
const closed = judged.filter((r) => r.cap === 0);
|
|
@@ -45835,17 +45857,14 @@ function classifyObservationScopeSaturation(reports) {
|
|
|
45835
45857
|
return {
|
|
45836
45858
|
name,
|
|
45837
45859
|
status: "fail",
|
|
45838
|
-
detail: `${failing.length} observation scope(s) at or over the cap \u2014 consolidation ` + `output is being discarded on them right now: ${list(failing)}` + (warning.length > 0 ? ` \u00b7 ${warning.length} more above ${Math.round(OBSERVATION_SCOPE_WARN_RATIO * 100)}%` : "") + backlog([...failing, ...warning]) + suffix,
|
|
45860
|
+
detail: `${failing.length} observation scope(s) at or over the cap \u2014 consolidation ` + `output is being discarded on them right now: ${list(failing)}` + (warning.length > 0 ? ` \u00b7 ${warning.length} more above ${Math.round(OBSERVATION_SCOPE_WARN_RATIO * 100)}%` : "") + backlog([...failing, ...warning]) + growthSuffix + suffix,
|
|
45839
45861
|
fix
|
|
45840
45862
|
};
|
|
45841
45863
|
}
|
|
45842
|
-
if (warning.length > 0) {
|
|
45843
|
-
|
|
45844
|
-
|
|
45845
|
-
|
|
45846
|
-
detail: `${warning.length} observation scope(s) above ` + `${Math.round(OBSERVATION_SCOPE_WARN_RATIO * 100)}% of the cap and still writable: ` + `${list(warning)}` + backlog(warning) + suffix,
|
|
45847
|
-
fix
|
|
45848
|
-
};
|
|
45864
|
+
if (warning.length > 0 || growing.length > 0) {
|
|
45865
|
+
const scopeClause = warning.length > 0 ? `${warning.length} observation scope(s) above ` + `${Math.round(OBSERVATION_SCOPE_WARN_RATIO * 100)}% of the cap and still writable: ` + `${list(warning)}` + backlog(warning) : "";
|
|
45866
|
+
const detail = warning.length > 0 ? scopeClause + growthSuffix + suffix : `${growthNote}${suffix}`;
|
|
45867
|
+
return { name, status: "warn", detail, ...warning.length > 0 ? { fix } : {} };
|
|
45849
45868
|
}
|
|
45850
45869
|
const totalScopes = capped.reduce((n, r) => n + r.scopeCount, 0);
|
|
45851
45870
|
const detailBase = capped.length === 0 ? `no bank has a positive observation cap` : `${totalScopes} scope(s) across ${capped.length} bank(s) below ` + `${Math.round(OBSERVATION_SCOPE_WARN_RATIO * 100)}% of the cap`;
|
|
@@ -45862,7 +45881,7 @@ function classifyObservationScopeSaturation(reports) {
|
|
|
45862
45881
|
detail: detailBase + (extra.length > 0 ? ` \u00b7 ${extra.join(" \u00b7 ")}` : "") + suffix
|
|
45863
45882
|
};
|
|
45864
45883
|
}
|
|
45865
|
-
async function
|
|
45884
|
+
async function getJson3(url, opts) {
|
|
45866
45885
|
const fetchImpl = opts?.fetchImpl ?? fetch;
|
|
45867
45886
|
const timeoutMs = opts?.timeoutMs ?? 15000;
|
|
45868
45887
|
const controller = new AbortController;
|
|
@@ -45891,10 +45910,11 @@ async function inspectBankScopes(apiUrl, bankId, opts) {
|
|
|
45891
45910
|
observationsEnabled: false,
|
|
45892
45911
|
hasScopeLimitRules: false,
|
|
45893
45912
|
scopeCount: 0,
|
|
45913
|
+
untaggedCount: 0,
|
|
45894
45914
|
saturated: [],
|
|
45895
45915
|
pendingConsolidation: null
|
|
45896
45916
|
});
|
|
45897
|
-
const cfg = await
|
|
45917
|
+
const cfg = await getJson3(`${base}/v1/default/banks/${bank}/config`, opts);
|
|
45898
45918
|
if (!cfg.ok)
|
|
45899
45919
|
return fail4(cfg.reason);
|
|
45900
45920
|
const config = cfg.data?.config;
|
|
@@ -45907,7 +45927,7 @@ async function inspectBankScopes(apiUrl, bankId, opts) {
|
|
|
45907
45927
|
const rules = config["observation_scope_limits"];
|
|
45908
45928
|
const hasScopeLimitRules = Array.isArray(rules) && rules.length > 0;
|
|
45909
45929
|
const observationsEnabled = config["enable_observations"] !== false;
|
|
45910
|
-
const scopesResp = await
|
|
45930
|
+
const scopesResp = await getJson3(`${base}/v1/default/banks/${bank}/observations/scopes`, opts);
|
|
45911
45931
|
if (!scopesResp.ok)
|
|
45912
45932
|
return fail4(scopesResp.reason);
|
|
45913
45933
|
const rawScopes = scopesResp.data?.scopes;
|
|
@@ -45924,9 +45944,10 @@ async function inspectBankScopes(apiUrl, bankId, opts) {
|
|
|
45924
45944
|
scopes.push({ tags, count });
|
|
45925
45945
|
}
|
|
45926
45946
|
const saturated = computeScopeSaturation(scopes, rawCap).filter((s) => s.status !== "ok");
|
|
45947
|
+
const untaggedCount = scopes.filter((s) => s.tags.length === 0).reduce((n, s) => n + s.count, 0);
|
|
45927
45948
|
let pendingConsolidation = null;
|
|
45928
45949
|
if (saturated.length > 0 && !hasScopeLimitRules && observationsEnabled) {
|
|
45929
|
-
const stats = await
|
|
45950
|
+
const stats = await getJson3(`${base}/v1/default/banks/${bank}/stats`, opts);
|
|
45930
45951
|
if (stats.ok && typeof stats.data?.pending_consolidation === "number") {
|
|
45931
45952
|
pendingConsolidation = stats.data.pending_consolidation;
|
|
45932
45953
|
}
|
|
@@ -45938,6 +45959,7 @@ async function inspectBankScopes(apiUrl, bankId, opts) {
|
|
|
45938
45959
|
observationsEnabled,
|
|
45939
45960
|
hasScopeLimitRules,
|
|
45940
45961
|
scopeCount: scopes.length,
|
|
45962
|
+
untaggedCount,
|
|
45941
45963
|
saturated,
|
|
45942
45964
|
pendingConsolidation
|
|
45943
45965
|
};
|
|
@@ -45953,7 +45975,7 @@ async function checkObservationScopeSaturation(config, apiUrl, opts) {
|
|
|
45953
45975
|
const reports = await Promise.all([...banks].map((bankId) => inspectBankScopes(apiUrl, bankId, opts)));
|
|
45954
45976
|
return classifyObservationScopeSaturation(reports);
|
|
45955
45977
|
}
|
|
45956
|
-
var OBSERVATION_SCOPE_CHECK_NAME = "hindsight observation scopes", OBSERVATION_SCOPE_WARN_RATIO = 0.8, OBSERVATION_SCOPE_MAX_LISTED = 6;
|
|
45978
|
+
var OBSERVATION_SCOPE_CHECK_NAME = "hindsight observation scopes", OBSERVATION_SCOPE_WARN_RATIO = 0.8, OBSERVATION_SCOPE_MAX_LISTED = 6, OBSERVATION_SCOPE_UNTAGGED_WARN_COUNT = 1e4;
|
|
45957
45979
|
var init_doctor_observation_scopes = __esm(() => {
|
|
45958
45980
|
init_merge();
|
|
45959
45981
|
init_hindsight2();
|
|
@@ -85474,6 +85496,174 @@ function reflectAcrossAgents(config) {
|
|
|
85474
85496
|
return { eligible, excluded, commands };
|
|
85475
85497
|
}
|
|
85476
85498
|
|
|
85499
|
+
// src/memory/observation-scope-cleanup.ts
|
|
85500
|
+
var CLEANUP_VOLATILE_PATTERNS = [
|
|
85501
|
+
/^parent_session:/,
|
|
85502
|
+
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}(-sub-.+)?$/
|
|
85503
|
+
];
|
|
85504
|
+
var CLEANUP_STALE_PATTERNS = [/^agent-[0-9a-f]{8,}$/];
|
|
85505
|
+
function matchesAny(tag, patterns) {
|
|
85506
|
+
return patterns.some((p) => p.test(tag));
|
|
85507
|
+
}
|
|
85508
|
+
function scopeKey(tags) {
|
|
85509
|
+
return tags.length === 0 ? "shared" : [...tags].sort().join(",");
|
|
85510
|
+
}
|
|
85511
|
+
function planScopeCleanup(bankId, scopes, options = {}) {
|
|
85512
|
+
const volatilePatterns = options.volatilePatterns ?? CLEANUP_VOLATILE_PATTERNS;
|
|
85513
|
+
const stalePatterns = options.stalePatterns ?? CLEANUP_STALE_PATTERNS;
|
|
85514
|
+
const retireStale = options.retireStale ?? false;
|
|
85515
|
+
const totalScopes = scopes.length;
|
|
85516
|
+
const totalObservations = scopes.reduce((n, s) => n + s.count, 0);
|
|
85517
|
+
const reclassified = [];
|
|
85518
|
+
const breakdown = {
|
|
85519
|
+
allVolatileScopes: 0,
|
|
85520
|
+
allVolatileObservations: 0,
|
|
85521
|
+
mixedScopes: 0,
|
|
85522
|
+
mixedObservations: 0,
|
|
85523
|
+
staleScopes: 0,
|
|
85524
|
+
staleObservations: 0
|
|
85525
|
+
};
|
|
85526
|
+
const targets = new Map;
|
|
85527
|
+
for (const s of scopes) {
|
|
85528
|
+
const volatileTags = s.tags.filter((t) => matchesAny(t, volatilePatterns));
|
|
85529
|
+
const staleTags = s.tags.filter((t) => matchesAny(t, stalePatterns));
|
|
85530
|
+
const stripped = s.tags.filter((t) => {
|
|
85531
|
+
if (matchesAny(t, volatilePatterns))
|
|
85532
|
+
return false;
|
|
85533
|
+
if (retireStale && matchesAny(t, stalePatterns))
|
|
85534
|
+
return false;
|
|
85535
|
+
return true;
|
|
85536
|
+
});
|
|
85537
|
+
const toKey = scopeKey(stripped);
|
|
85538
|
+
const changed = scopeKey(s.tags) !== toKey;
|
|
85539
|
+
const hasStable = s.tags.some((t) => !matchesAny(t, volatilePatterns) && !matchesAny(t, stalePatterns));
|
|
85540
|
+
if (staleTags.length > 0) {
|
|
85541
|
+
breakdown.staleScopes += 1;
|
|
85542
|
+
breakdown.staleObservations += s.count;
|
|
85543
|
+
}
|
|
85544
|
+
if (volatileTags.length > 0 && !hasStable && staleTags.length === 0) {
|
|
85545
|
+
breakdown.allVolatileScopes += 1;
|
|
85546
|
+
breakdown.allVolatileObservations += s.count;
|
|
85547
|
+
} else if (volatileTags.length > 0 && hasStable) {
|
|
85548
|
+
breakdown.mixedScopes += 1;
|
|
85549
|
+
breakdown.mixedObservations += s.count;
|
|
85550
|
+
}
|
|
85551
|
+
const entry = targets.get(toKey) ?? { sources: 0, observations: 0, changed: false };
|
|
85552
|
+
entry.sources += 1;
|
|
85553
|
+
entry.observations += s.count;
|
|
85554
|
+
entry.changed = entry.changed || changed;
|
|
85555
|
+
targets.set(toKey, entry);
|
|
85556
|
+
if (changed) {
|
|
85557
|
+
reclassified.push({
|
|
85558
|
+
from: s.tags,
|
|
85559
|
+
to: toKey,
|
|
85560
|
+
count: s.count,
|
|
85561
|
+
strippedVolatile: volatileTags,
|
|
85562
|
+
strippedStale: retireStale ? staleTags : []
|
|
85563
|
+
});
|
|
85564
|
+
}
|
|
85565
|
+
}
|
|
85566
|
+
reclassified.sort((a, b) => b.count - a.count);
|
|
85567
|
+
const merges = [...targets.entries()].filter(([, v]) => v.sources > 1 && v.changed).map(([target, v]) => ({ target, sourceScopes: v.sources, observations: v.observations })).sort((a, b) => b.observations - a.observations);
|
|
85568
|
+
const affectedObservations = reclassified.reduce((n, r) => n + r.count, 0);
|
|
85569
|
+
const resultingScopeCount = targets.size;
|
|
85570
|
+
return {
|
|
85571
|
+
bankId,
|
|
85572
|
+
ok: true,
|
|
85573
|
+
totalScopes,
|
|
85574
|
+
totalObservations,
|
|
85575
|
+
reclassified,
|
|
85576
|
+
merges,
|
|
85577
|
+
affectedScopes: reclassified.length,
|
|
85578
|
+
affectedObservations,
|
|
85579
|
+
resultingScopeCount,
|
|
85580
|
+
scopeReduction: totalScopes - resultingScopeCount,
|
|
85581
|
+
breakdown
|
|
85582
|
+
};
|
|
85583
|
+
}
|
|
85584
|
+
async function getJson2(url, opts) {
|
|
85585
|
+
const fetchImpl = opts?.fetchImpl ?? fetch;
|
|
85586
|
+
const timeoutMs = opts?.timeoutMs ?? 15000;
|
|
85587
|
+
const controller = new AbortController;
|
|
85588
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
85589
|
+
try {
|
|
85590
|
+
const resp = await fetchImpl(url, { signal: controller.signal });
|
|
85591
|
+
clearTimeout(timeout);
|
|
85592
|
+
if (!resp.ok)
|
|
85593
|
+
return { ok: false, reason: `HTTP ${resp.status}` };
|
|
85594
|
+
return { ok: true, data: await resp.json() };
|
|
85595
|
+
} catch (err) {
|
|
85596
|
+
clearTimeout(timeout);
|
|
85597
|
+
if (err.name === "AbortError")
|
|
85598
|
+
return { ok: false, reason: "Timeout" };
|
|
85599
|
+
return { ok: false, reason: String(err.message ?? err) };
|
|
85600
|
+
}
|
|
85601
|
+
}
|
|
85602
|
+
async function planBankCleanup(apiUrl, bankId, options = {}, fetchOpts) {
|
|
85603
|
+
const base = hindsightRestBase(apiUrl);
|
|
85604
|
+
const bank = encodeURIComponent(bankId);
|
|
85605
|
+
const resp = await getJson2(`${base}/v1/default/banks/${bank}/observations/scopes`, fetchOpts);
|
|
85606
|
+
if (!resp.ok) {
|
|
85607
|
+
return {
|
|
85608
|
+
bankId,
|
|
85609
|
+
ok: false,
|
|
85610
|
+
reason: resp.reason,
|
|
85611
|
+
totalScopes: 0,
|
|
85612
|
+
totalObservations: 0,
|
|
85613
|
+
reclassified: [],
|
|
85614
|
+
merges: [],
|
|
85615
|
+
affectedScopes: 0,
|
|
85616
|
+
affectedObservations: 0,
|
|
85617
|
+
resultingScopeCount: 0,
|
|
85618
|
+
scopeReduction: 0,
|
|
85619
|
+
breakdown: {
|
|
85620
|
+
allVolatileScopes: 0,
|
|
85621
|
+
allVolatileObservations: 0,
|
|
85622
|
+
mixedScopes: 0,
|
|
85623
|
+
mixedObservations: 0,
|
|
85624
|
+
staleScopes: 0,
|
|
85625
|
+
staleObservations: 0
|
|
85626
|
+
}
|
|
85627
|
+
};
|
|
85628
|
+
}
|
|
85629
|
+
const raw = resp.data?.scopes;
|
|
85630
|
+
const scopes = [];
|
|
85631
|
+
if (Array.isArray(raw)) {
|
|
85632
|
+
for (const entry of raw) {
|
|
85633
|
+
const tags = entry?.tags;
|
|
85634
|
+
const count = entry?.count;
|
|
85635
|
+
if (!Array.isArray(tags) || typeof count !== "number")
|
|
85636
|
+
continue;
|
|
85637
|
+
if (!tags.every((t) => typeof t === "string"))
|
|
85638
|
+
continue;
|
|
85639
|
+
scopes.push({ tags, count });
|
|
85640
|
+
}
|
|
85641
|
+
}
|
|
85642
|
+
return planScopeCleanup(bankId, scopes, options);
|
|
85643
|
+
}
|
|
85644
|
+
function formatCleanupPlan(plan, maxMerges = 10) {
|
|
85645
|
+
if (!plan.ok) {
|
|
85646
|
+
return `bank ${plan.bankId}: UNREADABLE (${plan.reason ?? "unknown"})`;
|
|
85647
|
+
}
|
|
85648
|
+
const b = plan.breakdown;
|
|
85649
|
+
const lines = [];
|
|
85650
|
+
lines.push(`bank ${plan.bankId}`);
|
|
85651
|
+
lines.push(` today: ${plan.totalScopes} scopes, ${plan.totalObservations.toLocaleString()} observations`);
|
|
85652
|
+
lines.push(` legacy fragmentation:` + ` all-volatile ${b.allVolatileScopes} scopes / ${b.allVolatileObservations.toLocaleString()} obs;` + ` mixed ${b.mixedScopes} scopes / ${b.mixedObservations.toLocaleString()} obs;` + ` stale agent-<hex> ${b.staleScopes} scopes / ${b.staleObservations.toLocaleString()} obs`);
|
|
85653
|
+
lines.push(` WOULD change: ${plan.affectedScopes} scopes` + ` (${plan.affectedObservations.toLocaleString()} obs re-homed),` + ` ${plan.totalScopes} \u2192 ${plan.resultingScopeCount} scopes` + ` (\u2212${plan.scopeReduction})`);
|
|
85654
|
+
if (plan.merges.length > 0) {
|
|
85655
|
+
lines.push(` top merges:`);
|
|
85656
|
+
for (const m of plan.merges.slice(0, maxMerges)) {
|
|
85657
|
+
lines.push(` ${m.sourceScopes} scopes \u2192 ${m.target} (${m.observations.toLocaleString()} obs)`);
|
|
85658
|
+
}
|
|
85659
|
+
if (plan.merges.length > maxMerges) {
|
|
85660
|
+
lines.push(` +${plan.merges.length - maxMerges} more merge targets`);
|
|
85661
|
+
}
|
|
85662
|
+
}
|
|
85663
|
+
return lines.join(`
|
|
85664
|
+
`);
|
|
85665
|
+
}
|
|
85666
|
+
|
|
85477
85667
|
// src/cli/memory.ts
|
|
85478
85668
|
init_helpers();
|
|
85479
85669
|
init_hindsight();
|
|
@@ -86200,6 +86390,55 @@ Demoting memory ${source_default.cyan(memoryId)}`));
|
|
|
86200
86390
|
` + " If the ID itself is suspect: `switchroom memory recall-log " + agent + " --json`."));
|
|
86201
86391
|
process.exit(1);
|
|
86202
86392
|
}));
|
|
86393
|
+
memory.command("cleanup-scopes [agent]").description("Dry-run audit of legacy pre-#4035 observation-scope fragmentation " + "(per-session UUID islands, stale agent-<hex> scopes). Reads only; " + "prints the blast radius of a curated re-home. Pass an agent, or --all.").option("--all", "Audit every agent bank and profile bank").option("--retire-stale", "Also fold stale agent-<hex> producer scopes into shared (opt-in; not part of the #4035 heal)").option("--execute", "(refused) live mutation is not supported \u2014 see the printed reason").option("--json", "Emit the plans as JSON").action(withConfigError(async (agent, opts) => {
|
|
86394
|
+
const config = getConfig(program3);
|
|
86395
|
+
const apiUrl = config.memory?.config?.url ?? HINDSIGHT_DEFAULT_MCP_URL;
|
|
86396
|
+
if (opts.execute) {
|
|
86397
|
+
console.error(source_default.red("\u2717 --execute is not supported: legacy-scope healing is not mechanizable"));
|
|
86398
|
+
console.error(source_default.gray(` Hindsight exposes no per-memory tag-write (update_memory / PATCH memories/{id}
|
|
86399
|
+
` + " silently drop a `tags` argument, #3772) and no per-scope delete (DELETE\n" + ` .../observations clears the WHOLE bank). So an observation's scope cannot be
|
|
86400
|
+
` + ` edited in place, and this tool stays a read-only audit. A live heal needs an
|
|
86401
|
+
` + ` upstream tag-write path or a deliberate, operator-approved enumerate-and-
|
|
86402
|
+
` + " invalidate pass \u2014 neither is run from here."));
|
|
86403
|
+
process.exit(1);
|
|
86404
|
+
}
|
|
86405
|
+
const banks = new Set;
|
|
86406
|
+
if (agent) {
|
|
86407
|
+
if (!config.agents[agent]) {
|
|
86408
|
+
console.error(source_default.red(`Agent "${agent}" is not defined in switchroom.yaml`));
|
|
86409
|
+
process.exit(1);
|
|
86410
|
+
}
|
|
86411
|
+
banks.add(getCollectionForAgent(agent, config));
|
|
86412
|
+
} else if (opts.all) {
|
|
86413
|
+
for (const name of Object.keys(config.agents)) {
|
|
86414
|
+
banks.add(getCollectionForAgent(name, config));
|
|
86415
|
+
}
|
|
86416
|
+
for (const bank of collectProfileBanks(config))
|
|
86417
|
+
banks.add(bank);
|
|
86418
|
+
} else {
|
|
86419
|
+
console.error(source_default.red("Pass an agent name, or --all to audit every bank."));
|
|
86420
|
+
process.exit(1);
|
|
86421
|
+
}
|
|
86422
|
+
const plans = await Promise.all([...banks].map((bankId) => planBankCleanup(apiUrl, bankId, { retireStale: opts.retireStale })));
|
|
86423
|
+
plans.sort((a, b) => b.affectedObservations - a.affectedObservations);
|
|
86424
|
+
if (opts.json) {
|
|
86425
|
+
console.log(JSON.stringify(plans, null, 2));
|
|
86426
|
+
return;
|
|
86427
|
+
}
|
|
86428
|
+
console.log(source_default.bold(`
|
|
86429
|
+
Observation-scope cleanup \u2014 DRY RUN (no writes)
|
|
86430
|
+
`));
|
|
86431
|
+
console.log(source_default.gray(` api=${apiUrl} retireStale=${Boolean(opts.retireStale)}
|
|
86432
|
+
`));
|
|
86433
|
+
for (const plan of plans) {
|
|
86434
|
+
console.log(formatCleanupPlan(plan));
|
|
86435
|
+
console.log();
|
|
86436
|
+
}
|
|
86437
|
+
const totalObs = plans.reduce((n, p) => n + p.affectedObservations, 0);
|
|
86438
|
+
const totalScopes = plans.reduce((n, p) => n + p.scopeReduction, 0);
|
|
86439
|
+
console.log(source_default.cyan(`Fleet total (would change): ${totalObs.toLocaleString()} observations re-homed, ` + `${totalScopes.toLocaleString()} scopes removed.`));
|
|
86440
|
+
console.log(source_default.gray("Nothing was written. Live execution is not supported from this tool " + "(--execute prints why)."));
|
|
86441
|
+
}));
|
|
86203
86442
|
const restBase = (url) => (url ?? HINDSIGHT_DEFAULT_MCP_URL).replace(/\/mcp\/?$/, "").replace(/\/$/, "");
|
|
86204
86443
|
const VALID_BANK = /^[a-zA-Z0-9_.-]+$/;
|
|
86205
86444
|
const profile = memory.command("profile").description("Author + inspect operator profile banks (shared/per-user memory; wire via memory.recall.additional_banks)");
|
|
@@ -93801,6 +94040,7 @@ init_atomic();
|
|
|
93801
94040
|
init_resolve_version();
|
|
93802
94041
|
init_audit_reader();
|
|
93803
94042
|
init_hindsight();
|
|
94043
|
+
init_hindsight2();
|
|
93804
94044
|
|
|
93805
94045
|
// src/cli/deploy-version-guard.ts
|
|
93806
94046
|
import { spawnSync as spawnSync18 } from "node:child_process";
|
|
@@ -93848,22 +94088,31 @@ function checkDowngrade(opts) {
|
|
|
93848
94088
|
init_agent_owned_tree();
|
|
93849
94089
|
init_component_versions();
|
|
93850
94090
|
var VERIFY_COMPONENTS_STEP = "verify-components";
|
|
94091
|
+
var ENSURE_BANKS_STEP = "ensure-banks";
|
|
93851
94092
|
function normalizeVersion(v) {
|
|
93852
94093
|
return v.trim().replace(/^v/, "");
|
|
93853
94094
|
}
|
|
93854
94095
|
function isVersionAssertable(target) {
|
|
93855
94096
|
return /^v?\d+\.\d+\.\d+$/.test(target.trim());
|
|
93856
94097
|
}
|
|
93857
|
-
|
|
94098
|
+
var ROLLOUT_PRIORITY_AGENTS = [
|
|
94099
|
+
"overlord",
|
|
94100
|
+
"carrie",
|
|
94101
|
+
"finn"
|
|
94102
|
+
];
|
|
94103
|
+
function orderAgentsForRoll(agents) {
|
|
93858
94104
|
const canary = agents.filter((a) => a === "test-harness");
|
|
93859
|
-
const
|
|
93860
|
-
|
|
94105
|
+
const priority = ROLLOUT_PRIORITY_AGENTS.filter((p) => p !== "test-harness" && agents.includes(p));
|
|
94106
|
+
const prioritySet = new Set(priority);
|
|
94107
|
+
const rest = agents.filter((a) => a !== "test-harness" && !prioritySet.has(a));
|
|
94108
|
+
return [...canary, ...priority, ...rest];
|
|
93861
94109
|
}
|
|
93862
94110
|
function planRollout(agents, opts = {}) {
|
|
93863
94111
|
const steps = [];
|
|
93864
|
-
const ordered =
|
|
94112
|
+
const ordered = orderAgentsForRoll(agents);
|
|
93865
94113
|
if (opts.hostdContext) {
|
|
93866
94114
|
steps.push({ kind: "apply" });
|
|
94115
|
+
steps.push({ kind: "refresh-hindsight" });
|
|
93867
94116
|
const [canary, ...rest] = ordered;
|
|
93868
94117
|
if (canary !== undefined) {
|
|
93869
94118
|
steps.push({ kind: "restart-agent", agent: canary });
|
|
@@ -93879,23 +94128,26 @@ function planRollout(agents, opts = {}) {
|
|
|
93879
94128
|
if (!opts.skipWeb) {
|
|
93880
94129
|
steps.push({ kind: "refresh-web" });
|
|
93881
94130
|
}
|
|
93882
|
-
steps.push({ kind: "refresh-hindsight" });
|
|
93883
94131
|
steps.push({ kind: "sweep" });
|
|
94132
|
+
steps.push({ kind: "ensure-banks" });
|
|
93884
94133
|
steps.push({ kind: "verify-components" });
|
|
93885
94134
|
return steps;
|
|
93886
94135
|
}
|
|
93887
94136
|
if (opts.pinToPersist)
|
|
93888
94137
|
steps.push({ kind: "persist-pin", pin: opts.pinToPersist });
|
|
93889
94138
|
steps.push({ kind: "apply" });
|
|
94139
|
+
if (!opts.skipWeb) {
|
|
94140
|
+
steps.push({ kind: "refresh-hindsight" });
|
|
94141
|
+
}
|
|
93890
94142
|
for (const agent of ordered) {
|
|
93891
94143
|
steps.push({ kind: "restart-agent", agent });
|
|
93892
94144
|
}
|
|
93893
94145
|
if (!opts.skipWeb) {
|
|
93894
94146
|
steps.push({ kind: "refresh-web" });
|
|
93895
94147
|
steps.push({ kind: "refresh-hostd" });
|
|
93896
|
-
steps.push({ kind: "refresh-hindsight" });
|
|
93897
94148
|
}
|
|
93898
94149
|
steps.push({ kind: "sweep" });
|
|
94150
|
+
steps.push({ kind: "ensure-banks" });
|
|
93899
94151
|
steps.push({ kind: "verify-components" });
|
|
93900
94152
|
return steps;
|
|
93901
94153
|
}
|
|
@@ -93926,6 +94178,9 @@ function formatRolloutPlan(steps, target) {
|
|
|
93926
94178
|
case "sweep":
|
|
93927
94179
|
lines.push(` ${n}. sweep \u2014 print per-agent version table`);
|
|
93928
94180
|
break;
|
|
94181
|
+
case "ensure-banks":
|
|
94182
|
+
lines.push(` ${n}. ensure-banks \u2014 create any missing agent Hindsight bank ` + `(idempotent backstop; structured failure if one stays missing)`);
|
|
94183
|
+
break;
|
|
93929
94184
|
case "verify-components":
|
|
93930
94185
|
lines.push(` ${n}. verify-components \u2014 assert EVERY in-scope component is on ${target} ` + `(same inventory \`switchroom update --check\` uses); non-zero exit if any is behind`);
|
|
93931
94186
|
break;
|
|
@@ -93987,7 +94242,7 @@ function evaluateRolloutDrift(components, target, steps, hostdContext) {
|
|
|
93987
94242
|
const { gated, exempt } = partitionDrift(report.behind, { owners, rolledAgents });
|
|
93988
94243
|
return { gated, exempt, target: report.target };
|
|
93989
94244
|
}
|
|
93990
|
-
function executeRollout(steps, target, deps, execOpts = {}) {
|
|
94245
|
+
async function executeRollout(steps, target, deps, execOpts = {}) {
|
|
93991
94246
|
const targetNorm = normalizeVersion(target);
|
|
93992
94247
|
const rolled = [];
|
|
93993
94248
|
const warnings = [];
|
|
@@ -93996,6 +94251,7 @@ function executeRollout(steps, target, deps, execOpts = {}) {
|
|
|
93996
94251
|
let agentIndex = 0;
|
|
93997
94252
|
let pinPersisted = false;
|
|
93998
94253
|
let driftVerdict = null;
|
|
94254
|
+
const bankEnsureFailed = [];
|
|
93999
94255
|
const fail4 = (partial) => {
|
|
94000
94256
|
let pinReverted = false;
|
|
94001
94257
|
if (pinPersisted && deps.revertPin) {
|
|
@@ -94100,6 +94356,10 @@ function executeRollout(steps, target, deps, execOpts = {}) {
|
|
|
94100
94356
|
const webTag = deps.webImageTag?.();
|
|
94101
94357
|
if (webTag !== undefined && webTag !== null && isVersionAssertable(webTag) && normalizeVersion(webTag) !== targetNorm) {
|
|
94102
94358
|
warnings.push(`web refresh completed but switchroom-web is running ${webTag}, ` + `NOT the roll target ${target} (most likely its downgrade guard ` + `skipped the install). Finish it host-side: \`switchroom webd ` + `install --tag ${target} --allow-downgrade\`.`);
|
|
94359
|
+
break;
|
|
94360
|
+
}
|
|
94361
|
+
if (webTag !== undefined && webTag !== null && isVersionAssertable(webTag) && normalizeVersion(webTag) === targetNorm) {
|
|
94362
|
+
emit({ phase: "web-refresh-done", target });
|
|
94103
94363
|
}
|
|
94104
94364
|
break;
|
|
94105
94365
|
}
|
|
@@ -94115,9 +94375,11 @@ function executeRollout(steps, target, deps, execOpts = {}) {
|
|
|
94115
94375
|
const hindsightExists = deps.hindsightExists ?? isHindsightContainerExists;
|
|
94116
94376
|
if (!hindsightExists()) {
|
|
94117
94377
|
deps.log(`ROLL_STEP refresh-hindsight \u2014 no hindsight container on this host; skipping`);
|
|
94378
|
+
emit({ phase: "hindsight-skipped", target });
|
|
94118
94379
|
break;
|
|
94119
94380
|
}
|
|
94120
94381
|
deps.log(`ROLL_STEP refresh-hindsight \u2014 memory setup --recreate --tag ${target} (pull ${target} + recreate)`);
|
|
94382
|
+
emit({ phase: "hindsight-refresh", target });
|
|
94121
94383
|
const r = deps.run(["memory", "setup", "--recreate", "--tag", target]);
|
|
94122
94384
|
if (r.status !== 0) {
|
|
94123
94385
|
deps.log(` \u2717 hindsight recreate FAILED \u2014 the memory backend is DOWN (the ` + `recreate stopped + removed the old container before the failed ` + `start). Run \`switchroom memory setup\` to bring it back. \u2014 STOPPING`);
|
|
@@ -94126,6 +94388,7 @@ function executeRollout(steps, target, deps, execOpts = {}) {
|
|
|
94126
94388
|
...r.timedOut ? { timedOut: true } : {}
|
|
94127
94389
|
});
|
|
94128
94390
|
}
|
|
94391
|
+
emit({ phase: "hindsight-refresh-done", target });
|
|
94129
94392
|
break;
|
|
94130
94393
|
}
|
|
94131
94394
|
case "sweep": {
|
|
@@ -94136,6 +94399,38 @@ function executeRollout(steps, target, deps, execOpts = {}) {
|
|
|
94136
94399
|
}
|
|
94137
94400
|
break;
|
|
94138
94401
|
}
|
|
94402
|
+
case "ensure-banks": {
|
|
94403
|
+
const hindsightExists = deps.hindsightExists ?? isHindsightContainerExists;
|
|
94404
|
+
if (!hindsightExists()) {
|
|
94405
|
+
deps.log(`ROLL_STEP ensure-banks \u2014 no hindsight container on this host; skipping`);
|
|
94406
|
+
break;
|
|
94407
|
+
}
|
|
94408
|
+
if (!deps.ensureBank) {
|
|
94409
|
+
warnings.push(`ensure-banks ran with NO bank-ensure hook wired, so brand-new ` + `agents' Hindsight banks were NOT verified. If an agent's first ` + `\`retain\` fails with a foreign-key error, its bank was never ` + `created \u2014 recreate it by restarting that agent.`);
|
|
94410
|
+
break;
|
|
94411
|
+
}
|
|
94412
|
+
deps.log(`ROLL_STEP ensure-banks \u2014 creating any missing agent bank (idempotent) for ${rolled.length} agent(s)`);
|
|
94413
|
+
for (const a of rolled) {
|
|
94414
|
+
let res;
|
|
94415
|
+
try {
|
|
94416
|
+
res = await deps.ensureBank(a);
|
|
94417
|
+
} catch (e) {
|
|
94418
|
+
res = { ok: false, reason: e.message };
|
|
94419
|
+
}
|
|
94420
|
+
if (res.ok) {
|
|
94421
|
+
deps.log(` ${a}: bank present`);
|
|
94422
|
+
continue;
|
|
94423
|
+
}
|
|
94424
|
+
if (/unreachable|timeout/i.test(res.reason)) {
|
|
94425
|
+
deps.log(` ${a}: bank check ${res.reason.toLowerCase()} (degraded to warning)`);
|
|
94426
|
+
warnings.push(`ensure-banks could not reach Hindsight to verify ${a}'s bank ` + `(${res.reason}). If ${a} is brand-new and its first \`retain\` ` + `fails with a foreign-key error, restart it to recreate the bank.`);
|
|
94427
|
+
continue;
|
|
94428
|
+
}
|
|
94429
|
+
deps.log(` \u2717 ${a}: bank could NOT be created \u2014 ${res.reason}`);
|
|
94430
|
+
bankEnsureFailed.push(a);
|
|
94431
|
+
}
|
|
94432
|
+
break;
|
|
94433
|
+
}
|
|
94139
94434
|
case "verify-components": {
|
|
94140
94435
|
deps.log(`ROLL_STEP verify-components \u2014 asserting every in-scope component is on ${target}`);
|
|
94141
94436
|
if (!deps.collectComponents) {
|
|
@@ -94174,6 +94469,19 @@ function executeRollout(steps, target, deps, execOpts = {}) {
|
|
|
94174
94469
|
warnings.push(`roll succeeded but committing the durable release.pin threw: ` + `${e.message}. The pin IS written; a stale rollout ` + `journal may cause a later boot to revert it \u2014 verify host-side.`);
|
|
94175
94470
|
}
|
|
94176
94471
|
}
|
|
94472
|
+
if (bankEnsureFailed.length > 0) {
|
|
94473
|
+
for (const a of bankEnsureFailed) {
|
|
94474
|
+
warnings.push(`${a}'s Hindsight bank could NOT be created during the roll \u2014 its ` + `first \`retain\` will fail with a foreign-key error until the bank ` + `exists. Restart ${a} to re-run its bank creation now that Hindsight ` + `is back up.`);
|
|
94475
|
+
}
|
|
94476
|
+
deps.log(` \u2717 ensure-banks FAILED \u2014 ${bankEnsureFailed.length} agent bank(s) ` + `could not be created: ${bankEnsureFailed.join(", ")}`);
|
|
94477
|
+
return {
|
|
94478
|
+
ok: false,
|
|
94479
|
+
rolled,
|
|
94480
|
+
warnings,
|
|
94481
|
+
failedStep: ENSURE_BANKS_STEP,
|
|
94482
|
+
drifted: bankEnsureFailed
|
|
94483
|
+
};
|
|
94484
|
+
}
|
|
94177
94485
|
if (driftVerdict && driftVerdict.gated.length > 0) {
|
|
94178
94486
|
const normalizedTarget = `v${normalizeVersion(target)}`;
|
|
94179
94487
|
for (const c of driftVerdict.gated) {
|
|
@@ -94340,7 +94648,12 @@ function createRolloutDeps(params) {
|
|
|
94340
94648
|
`);
|
|
94341
94649
|
}
|
|
94342
94650
|
return outcome.reverted;
|
|
94343
|
-
}
|
|
94651
|
+
},
|
|
94652
|
+
...params.hindsightApiUrl && params.resolveBankId ? {
|
|
94653
|
+
ensureBank: (agent) => createBank(params.hindsightApiUrl, params.resolveBankId(agent), {
|
|
94654
|
+
timeoutMs: 5000
|
|
94655
|
+
})
|
|
94656
|
+
} : {}
|
|
94344
94657
|
};
|
|
94345
94658
|
}
|
|
94346
94659
|
function registerRolloutCommand(program3) {
|
|
@@ -94433,10 +94746,23 @@ function registerRolloutCommand(program3) {
|
|
|
94433
94746
|
}
|
|
94434
94747
|
const configPath = getConfigPath(program3);
|
|
94435
94748
|
const scriptPath = process.argv[1] ?? "";
|
|
94436
|
-
|
|
94749
|
+
let hindsightApiUrl;
|
|
94750
|
+
let resolveBankId;
|
|
94751
|
+
if (isHindsightEnabled(config)) {
|
|
94752
|
+
const rawUrl = config.memory?.config?.url;
|
|
94753
|
+
hindsightApiUrl = rawUrl ? rawUrl.replace(/\/mcp\/?$/, "").replace(/\/$/, "") : HINDSIGHT_DEFAULT_API_BASE_URL;
|
|
94754
|
+
resolveBankId = (agent) => config.agents?.[agent]?.memory?.collection ?? agent;
|
|
94755
|
+
}
|
|
94756
|
+
const deps = createRolloutDeps({
|
|
94757
|
+
configPath,
|
|
94758
|
+
scriptPath,
|
|
94759
|
+
hostdCtx,
|
|
94760
|
+
hindsightApiUrl,
|
|
94761
|
+
resolveBankId
|
|
94762
|
+
});
|
|
94437
94763
|
process.stdout.write(`${opts.allowDowngrade ? "Rolling back" : "Rolling"} ${requested.length} agent(s) to ${target}\u2026
|
|
94438
94764
|
`);
|
|
94439
|
-
const result = executeRollout(steps, target, deps, {
|
|
94765
|
+
const result = await executeRollout(steps, target, deps, {
|
|
94440
94766
|
hostdContext: hostdCtx,
|
|
94441
94767
|
allowDowngrade: opts.allowDowngrade
|
|
94442
94768
|
});
|
|
@@ -94457,6 +94783,15 @@ function registerRolloutCommand(program3) {
|
|
|
94457
94783
|
\u2717 Rollout INCOMPLETE \u2014 ${(result.drifted ?? []).length} component(s) ` + `still behind ${target}: ${(result.drifted ?? []).join(", ") || "unknown"}.
|
|
94458
94784
|
` + ` ${result.rolled.length} agent(s) DID reach ${target} and the pin is ` + `committed \u2014 re-running the agent roll will not fix this.
|
|
94459
94785
|
` + ` Run the per-component command named in the warning(s) above, then ` + `\`switchroom update --check\` to confirm the host is converged.
|
|
94786
|
+
`);
|
|
94787
|
+
process.exitCode = 1;
|
|
94788
|
+
return;
|
|
94789
|
+
}
|
|
94790
|
+
if (!result.ok && result.failedStep === ENSURE_BANKS_STEP) {
|
|
94791
|
+
process.stderr.write(`
|
|
94792
|
+
\u2717 Rollout INCOMPLETE \u2014 ${(result.drifted ?? []).length} agent(s) ` + `reached ${target} but their Hindsight bank could NOT be created: ` + `${(result.drifted ?? []).join(", ") || "unknown"}.
|
|
94793
|
+
` + ` The pin is committed and the agents ARE on the target \u2014 re-running ` + `the agent roll will not fix this.
|
|
94794
|
+
` + ` Restart the named agent(s) to re-run bank creation now that ` + `Hindsight is back up (their first \`retain\` fails with a foreign-key ` + `error until the bank exists).
|
|
94460
94795
|
`);
|
|
94461
94796
|
process.exitCode = 1;
|
|
94462
94797
|
return;
|