@mnmal-ai/cortext 1.0.0-alpha.1 → 1.0.0-alpha.10

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/bootstrap.js CHANGED
@@ -1,5 +1,5 @@
1
- import { E as isMonotonicAuth, t as createCortextPlugin, w as DEFAULT_AUTH } from "./src-BKaGTnRw.js";
2
- import { D as ARCHITECTURE_NAMESPACE, k as CORTEXT_NAMESPACE } from "./plugin-27UpMnoE.js";
1
+ import { E as isMonotonicAuth, t as createCortextPlugin, w as DEFAULT_AUTH } from "./src-DAeiNhDw.js";
2
+ import { A as CORTEXT_NAMESPACE, O as ARCHITECTURE_NAMESPACE } from "./plugin-CujP_pvT.js";
3
3
  import "./kernel.js";
4
4
  import { defineBootstrap } from "@mnmal-ai/substrate/sdk";
5
5
  import { z } from "zod";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { C as runCompact, E as isMonotonicAuth, S as registerCompactRoute, T as buildAuthPolicies, _ as TODO_RESOLUTION_ALIASES, a as classSatisfies, b as buildArchitectureNamespace, c as AGENT_RULE_CATEGORY_VOCAB, d as ARCHITECTURE_TYPE_NAMES, f as CLAUDE_TYPE_NAMES, g as HOST_INSTALL_FRAGMENT_STATES, h as HOST_INSTALL_CHECK_STATES, i as bumpClass, l as AGENT_RULE_SEVERITY_ALIASES, m as DECISION_STATUS_VOCAB, n as inferTodoResolution, o as classifySurfaceChange, p as DECISION_STATUS_ALIASES, r as buildReleaseSurface, s as registerOnboardRoute, t as createCortextPlugin, u as AGENT_RULE_SEVERITY_VOCAB, v as TODO_STATUS_ALIASES, w as DEFAULT_AUTH, x as buildClaudeNamespace, y as TODO_STATUS_IMPLIES } from "./src-BKaGTnRw.js";
2
- import { g as AGENT_MESSAGE_STATUS_VOCAB } from "./plugin-27UpMnoE.js";
1
+ import { C as runCompact, E as isMonotonicAuth, S as registerCompactRoute, T as buildAuthPolicies, _ as TODO_RESOLUTION_ALIASES, a as classSatisfies, b as buildArchitectureNamespace, c as AGENT_RULE_CATEGORY_VOCAB, d as ARCHITECTURE_TYPE_NAMES, f as CLAUDE_TYPE_NAMES, g as HOST_INSTALL_FRAGMENT_STATES, h as HOST_INSTALL_CHECK_STATES, i as bumpClass, l as AGENT_RULE_SEVERITY_ALIASES, m as DECISION_STATUS_VOCAB, n as inferTodoResolution, o as classifySurfaceChange, p as DECISION_STATUS_ALIASES, r as buildReleaseSurface, s as registerOnboardRoute, t as createCortextPlugin, u as AGENT_RULE_SEVERITY_VOCAB, v as TODO_STATUS_ALIASES, w as DEFAULT_AUTH, x as buildClaudeNamespace, y as TODO_STATUS_IMPLIES } from "./src-DAeiNhDw.js";
2
+ import { _ as AGENT_MESSAGE_STATUS_VOCAB } from "./plugin-CujP_pvT.js";
3
3
  import "./kernel.js";
4
4
 
5
5
  export { AGENT_MESSAGE_STATUS_VOCAB, AGENT_RULE_CATEGORY_VOCAB, AGENT_RULE_SEVERITY_ALIASES, AGENT_RULE_SEVERITY_VOCAB, ARCHITECTURE_TYPE_NAMES, CLAUDE_TYPE_NAMES, DECISION_STATUS_ALIASES, DECISION_STATUS_VOCAB, DEFAULT_AUTH, HOST_INSTALL_CHECK_STATES, HOST_INSTALL_FRAGMENT_STATES, TODO_RESOLUTION_ALIASES, TODO_STATUS_ALIASES, TODO_STATUS_IMPLIES, buildArchitectureNamespace, buildAuthPolicies, buildClaudeNamespace, buildReleaseSurface, bumpClass, classSatisfies, classifySurfaceChange, createCortextPlugin, inferTodoResolution, isMonotonicAuth, registerCompactRoute, registerOnboardRoute, runCompact };
@@ -45,12 +45,12 @@
45
45
  export declare const CORTEXT_NAMESPACE = "cortext";
46
46
  /**
47
47
  * The namespace the architecture-findings surface registers under, and which
48
- * migration 0002 scopes its body normalization to.
48
+ * the pre-baseline body-normalization migration was scoped to.
49
49
  *
50
50
  * Separate from CORTEXT_NAMESPACE because the two have separate operator
51
51
  * knobs and diverging lifecycle policies (compaction, retention, audit) — but
52
52
  * subject to exactly the same gap, which is why it is stated here rather than
53
- * left as a bare literal inside 0002.
53
+ * left as a bare literal inside that migration.
54
54
  */
55
55
  export declare const ARCHITECTURE_NAMESPACE = "architecture";
56
56
  /**
@@ -8,8 +8,9 @@
8
8
  export { ARCHITECTURE_NAMESPACE, CORTEXT_COORDINATOR_NAME, CORTEXT_NAMESPACE } from './cortext-namespace.js';
9
9
  export { auditNamespaceCoverage, auditObjects, type Finding, findingMessage, INVARIANT_OBJECTS, type InvariantObject, type PluginNamespace, reportInvariantAudit, resolveCortextNamespaces, scopedNamespace, type UntargetableReason } from './invariant-audit.js';
10
10
  export { cortextBaselineMigration } from './migrations/0001_baseline.js';
11
+ export { cortextDropEngineOwnedIndexesMigration, ENGINE_OWNED_INDEXES } from './migrations/0002_drop_engine_owned_indexes.js';
11
12
  export { createCortextKernelPlugin } from './plugin.js';
12
13
  export { REPAIR_ENV_VAR, type RepairOutcome, repairIfRequested, repairInvariants, repairOutcomeMessage, repairRequested } from './repair.js';
13
- export { countDuplicateHostInstallKeys, countDuplicateKids, rescopeHostInstallKeyIndex, rescopeKidUniqueIndex, rescopeRecipientIndex, rescopeVocabConstraint, VOCAB_CONSTRAINTS, type VocabConstraint } from './rescope.js';
14
+ export { countDuplicateHostInstallKeys, countDuplicateKids, rescopeHostInstallKeyIndex, rescopeKidUniqueIndex, rescopeVocabConstraint, VOCAB_CONSTRAINTS, type VocabConstraint } from './rescope.js';
14
15
  export { SUBSTRATE_CONTRACT_RANGE } from './substrate-contract.js';
15
16
  export { AGENT_MESSAGE_STATUS_VOCAB, TODO_LIFECYCLE_VOCAB, TODO_PRIORITY_VOCAB, TODO_RESOLUTION_VOCAB } from './todo-vocab.js';
@@ -51,8 +51,8 @@ export interface InvariantObject {
51
51
  * if this list and the migrations disagree — which is the check that actually
52
52
  * holds them together.
53
53
  *
54
- * Note there is no `architecture/*` entry, and that is correct: migration 0002
55
- * is a one-time body normalisation, not a standing invariant. A drifted
54
+ * Note there is no `architecture/*` entry, and that is correct: the
55
+ * pre-baseline architecture migration was a one-time body normalisation, not a standing invariant. A drifted
56
56
  * architecture namespace means that normalisation never ran for those rows —
57
57
  * real, but a different and lesser thing than "kid uniqueness is not held".
58
58
  */
@@ -89,6 +89,7 @@ export type Finding =
89
89
  readonly kind: 'object_misscoped';
90
90
  readonly object: InvariantObject;
91
91
  readonly scopedTo: string | null;
92
+ readonly expected: string;
92
93
  }
93
94
  /** Present and correctly scoped, but NOT VALID — new writes only. */
94
95
  | {
@@ -148,7 +149,7 @@ export declare function scopedNamespace(definition: string): string | null;
148
149
  * gains a filter, an early return or a per-object skip, the completion metric
149
150
  * reports what was actually inspected instead of a promise.
150
151
  */
151
- export declare function auditObjects(sql: unknown): Promise<{
152
+ export declare function auditObjects(sql: unknown, target?: string): Promise<{
152
153
  findings: Finding[];
153
154
  checked: number;
154
155
  }>;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Which storage layout `rows` has: one shared table (`legacy`) or a table
3
+ * partitioned by namespace and type (`partitioned`).
4
+ *
5
+ * On a partitioned `rows`, Postgres refuses a UNIQUE index that lacks the
6
+ * partition keys, and clones every parent-level index and CHECK onto every
7
+ * other plugin's partitions. So cortext's raw DDL runs on `legacy` only; on
8
+ * `partitioned` the engine builds cortext's declared and `indexed: true`
9
+ * indexes per partition.
10
+ */
11
+ export type StoreLayout = 'legacy' | 'partitioned';
12
+ /**
13
+ * The layout as the catalog reports it. Migrations read `ctx.layout` instead;
14
+ * this is for boot-time code, whose lifecycle context does not carry it.
15
+ */
16
+ export declare function storeLayout(sql: unknown): Promise<StoreLayout>;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Drops the hand-made indexes 0001 used to build on a legacy `rows`, now that
3
+ * the engine serves the same reads: the derived recipient index for the
4
+ * inbox, and the `indexed: true` indexes on project and status.
5
+ *
6
+ * A partitioned `rows` never had them. Idempotent, like 0001: the runner does
7
+ * not wrap `apply` in a transaction.
8
+ */
9
+ import type { KernelMigration } from '@mnmal-ai/substrate/sdk';
10
+ export declare const ENGINE_OWNED_INDEXES: readonly ['rows_agent_message_recipient_idx', 'rows_ns_type_project_idx', 'rows_ns_type_status_idx', 'rows_ns_type_priority_idx', 'rows_cold_start_idx'];
11
+ export declare const cortextDropEngineOwnedIndexesMigration: KernelMigration;
@@ -41,14 +41,6 @@ export declare const VOCAB_CONSTRAINTS: readonly VocabConstraint[];
41
41
  * enough to accumulate bad rows.
42
42
  */
43
43
  export declare function rescopeVocabConstraint(s: Sql, metric: Metric, { constraint, type, column, vocab }: VocabConstraint, namespace: string): Promise<number>;
44
- /**
45
- * Re-scope the mailbox inbox index (0003) to `namespace`.
46
- *
47
- * Dropped and rebuilt rather than left alongside a correctly-scoped sibling:
48
- * two partial indexes over the same expression is one the planner can never
49
- * use, plus write amplification on every AgentMessage insert.
50
- */
51
- export declare function rescopeRecipientIndex(s: Sql, namespace: string): Promise<void>;
52
44
  /**
53
45
  * How many kids would collide if the unique index were scoped to `namespace`.
54
46
  *
@@ -16,4 +16,4 @@
16
16
  * by reflex: widen it only after reading what the new major changed.
17
17
  * `substrate-contract-parity.test.ts` holds the manifest to this literal.
18
18
  */
19
- export declare const SUBSTRATE_CONTRACT_RANGE = ">=1.0.0-alpha.0 <2.0.0";
19
+ export declare const SUBSTRATE_CONTRACT_RANGE = ">=1.0.0-alpha.10 <2.0.0";
@@ -1,4 +1,4 @@
1
- import { t as createCortextKernelPlugin } from "./plugin-27UpMnoE.js";
1
+ import { t as createCortextKernelPlugin } from "./plugin-CujP_pvT.js";
2
2
  import { defineBootstrap } from "@mnmal-ai/substrate/sdk";
3
3
  import { z } from "zod";
4
4
 
package/dist/kernel.js CHANGED
@@ -1,3 +1,3 @@
1
- import { C as findingMessage, D as ARCHITECTURE_NAMESPACE, E as scopedNamespace, O as CORTEXT_COORDINATOR_NAME, S as auditObjects, T as resolveCortextNamespaces, _ as TODO_LIFECYCLE_VOCAB, a as repairInvariants, b as INVARIANT_OBJECTS, c as cortextBaselineMigration, d as countDuplicateKids, f as rescopeHostInstallKeyIndex, g as AGENT_MESSAGE_STATUS_VOCAB, h as rescopeVocabConstraint, i as repairIfRequested, k as CORTEXT_NAMESPACE, l as VOCAB_CONSTRAINTS, m as rescopeRecipientIndex, n as SUBSTRATE_CONTRACT_RANGE, o as repairOutcomeMessage, p as rescopeKidUniqueIndex, r as REPAIR_ENV_VAR, s as repairRequested, t as createCortextKernelPlugin, u as countDuplicateHostInstallKeys, v as TODO_PRIORITY_VOCAB, w as reportInvariantAudit, x as auditNamespaceCoverage, y as TODO_RESOLUTION_VOCAB } from "./plugin-27UpMnoE.js";
1
+ import { A as CORTEXT_NAMESPACE, C as auditObjects, D as scopedNamespace, E as resolveCortextNamespaces, O as ARCHITECTURE_NAMESPACE, S as auditNamespaceCoverage, T as reportInvariantAudit, _ as AGENT_MESSAGE_STATUS_VOCAB, a as repairInvariants, b as TODO_RESOLUTION_VOCAB, c as ENGINE_OWNED_INDEXES, d as VOCAB_CONSTRAINTS, f as countDuplicateHostInstallKeys, g as rescopeVocabConstraint, h as rescopeKidUniqueIndex, i as repairIfRequested, k as CORTEXT_COORDINATOR_NAME, l as cortextDropEngineOwnedIndexesMigration, m as rescopeHostInstallKeyIndex, n as SUBSTRATE_CONTRACT_RANGE, o as repairOutcomeMessage, p as countDuplicateKids, r as REPAIR_ENV_VAR, s as repairRequested, t as createCortextKernelPlugin, u as cortextBaselineMigration, v as TODO_LIFECYCLE_VOCAB, w as findingMessage, x as INVARIANT_OBJECTS, y as TODO_PRIORITY_VOCAB } from "./plugin-CujP_pvT.js";
2
2
 
3
- export { AGENT_MESSAGE_STATUS_VOCAB, ARCHITECTURE_NAMESPACE, CORTEXT_COORDINATOR_NAME, CORTEXT_NAMESPACE, INVARIANT_OBJECTS, REPAIR_ENV_VAR, SUBSTRATE_CONTRACT_RANGE, TODO_LIFECYCLE_VOCAB, TODO_PRIORITY_VOCAB, TODO_RESOLUTION_VOCAB, VOCAB_CONSTRAINTS, auditNamespaceCoverage, auditObjects, cortextBaselineMigration, countDuplicateHostInstallKeys, countDuplicateKids, createCortextKernelPlugin, findingMessage, repairIfRequested, repairInvariants, repairOutcomeMessage, repairRequested, reportInvariantAudit, rescopeHostInstallKeyIndex, rescopeKidUniqueIndex, rescopeRecipientIndex, rescopeVocabConstraint, resolveCortextNamespaces, scopedNamespace };
3
+ export { AGENT_MESSAGE_STATUS_VOCAB, ARCHITECTURE_NAMESPACE, CORTEXT_COORDINATOR_NAME, CORTEXT_NAMESPACE, ENGINE_OWNED_INDEXES, INVARIANT_OBJECTS, REPAIR_ENV_VAR, SUBSTRATE_CONTRACT_RANGE, TODO_LIFECYCLE_VOCAB, TODO_PRIORITY_VOCAB, TODO_RESOLUTION_VOCAB, VOCAB_CONSTRAINTS, auditNamespaceCoverage, auditObjects, cortextBaselineMigration, cortextDropEngineOwnedIndexesMigration, countDuplicateHostInstallKeys, countDuplicateKids, createCortextKernelPlugin, findingMessage, repairIfRequested, repairInvariants, repairOutcomeMessage, repairRequested, reportInvariantAudit, rescopeHostInstallKeyIndex, rescopeKidUniqueIndex, rescopeVocabConstraint, resolveCortextNamespaces, scopedNamespace };
@@ -21,12 +21,26 @@ export declare function isOperator(ctx: RequestContext): boolean;
21
21
  * neither is a bystander's call.
22
22
  */
23
23
  export declare function messageParticipantsOnly(installCtx: NamespaceInstallContext, name: string, inner: RegistryEntryResolver): RegistryEntryResolver;
24
+ /**
25
+ * AgentIdentity create: a non-operator may not file a row whose `host` is a
26
+ * machine other than the one its kid names. The fleet audit trusts `host`
27
+ * (and `roaming` on that host) when it decides who may write a host's
28
+ * HostInstall rows.
29
+ */
30
+ export declare function identityCreateGuard(installCtx: NamespaceInstallContext, name: string, inner: RegistryEntryResolver): RegistryEntryResolver;
24
31
  /**
25
32
  * AgentIdentity update: the identity itself (the caller's kid is the row's
26
33
  * kid) or an operator. `kid` is the row's identity, so changing it is an
27
34
  * operator act even on your own row: otherwise a row could be renamed onto a
28
- * kid whose mail it would then receive.
35
+ * kid whose mail it would then receive. `host` must stay the kid's machine,
36
+ * and reactivating a retired row is an operator act.
29
37
  */
30
38
  export declare function identitySelfOnly(installCtx: NamespaceInstallContext, name: string, inner: RegistryEntryResolver): RegistryEntryResolver;
39
+ /**
40
+ * Todo create/update: `resolution` says why a CLOSED Todo stopped, so a live
41
+ * Todo carries none. A resolution with a live resulting status is refused,
42
+ * and moving a Todo back to a live status clears the one it had.
43
+ */
44
+ export declare function todoResolutionGuard(installCtx: NamespaceInstallContext, name: string, inner: RegistryEntryResolver, statusAliases: Readonly<Record<string, string>>): RegistryEntryResolver;
31
45
  /** AgentIdentity delete: operators only. Mail hangs off these rows. */
32
46
  export declare function operatorOnly(name: string, inner: RegistryEntryResolver): RegistryEntryResolver;
@@ -46,12 +46,12 @@
46
46
  const CORTEXT_NAMESPACE = "cortext";
47
47
  /**
48
48
  * The namespace the architecture-findings surface registers under, and which
49
- * migration 0002 scopes its body normalization to.
49
+ * the pre-baseline body-normalization migration was scoped to.
50
50
  *
51
51
  * Separate from CORTEXT_NAMESPACE because the two have separate operator
52
52
  * knobs and diverging lifecycle policies (compaction, retention, audit) — but
53
53
  * subject to exactly the same gap, which is why it is stated here rather than
54
- * left as a bare literal inside 0002.
54
+ * left as a bare literal inside that migration.
55
55
  */
56
56
  const ARCHITECTURE_NAMESPACE = "architecture";
57
57
  /**
@@ -96,6 +96,19 @@ const CORTEXT_PROMOTED_COLUMNS = {
96
96
  }
97
97
  };
98
98
 
99
+ //#endregion
100
+ //#region src/kernel/layout.ts
101
+ /**
102
+ * The layout as the catalog reports it. Migrations read `ctx.layout` instead;
103
+ * this is for boot-time code, whose lifecycle context does not carry it.
104
+ */
105
+ async function storeLayout(sql) {
106
+ const [row] = await sql`
107
+ SELECT c.relkind AS kind FROM pg_class c WHERE c.oid = to_regclass('rows')
108
+ `;
109
+ return row?.kind === "p" ? "partitioned" : "legacy";
110
+ }
111
+
99
112
  //#endregion
100
113
  //#region src/kernel/invariant-audit.ts
101
114
  /**
@@ -141,8 +154,8 @@ const CORTEXT_PROMOTED_COLUMNS = {
141
154
  * if this list and the migrations disagree — which is the check that actually
142
155
  * holds them together.
143
156
  *
144
- * Note there is no `architecture/*` entry, and that is correct: migration 0002
145
- * is a one-time body normalisation, not a standing invariant. A drifted
157
+ * Note there is no `architecture/*` entry, and that is correct: the
158
+ * pre-baseline architecture migration was a one-time body normalisation, not a standing invariant. A drifted
146
159
  * architecture namespace means that normalisation never ran for those rows —
147
160
  * real, but a different and lesser thing than "kid uniqueness is not held".
148
161
  */
@@ -159,12 +172,6 @@ const INVARIANT_OBJECTS = [
159
172
  migration: "0001_baseline",
160
173
  guarantee: "Todo.priority is restricted to its controlled vocabulary"
161
174
  },
162
- {
163
- name: "rows_agent_message_recipient_idx",
164
- kind: "index",
165
- migration: "0001_baseline",
166
- guarantee: "mailbox inbox reads are index-backed rather than heap scans"
167
- },
168
175
  {
169
176
  name: "rows_agent_identity_kid_unique",
170
177
  kind: "index",
@@ -280,7 +287,7 @@ async function readCatalog(sql) {
280
287
  * gains a filter, an early return or a per-object skip, the completion metric
281
288
  * reports what was actually inspected instead of a promise.
282
289
  */
283
- async function auditObjects(sql) {
290
+ async function auditObjects(sql, target = CORTEXT_NAMESPACE) {
284
291
  const catalog = await readCatalog(sql);
285
292
  const findings = [];
286
293
  let checked = 0;
@@ -295,11 +302,12 @@ async function auditObjects(sql) {
295
302
  continue;
296
303
  }
297
304
  const scope = scopedNamespace(row.definition);
298
- if (scope !== "cortext") {
305
+ if (scope !== target) {
299
306
  findings.push({
300
307
  kind: "object_misscoped",
301
308
  object,
302
- scopedTo: scope
309
+ scopedTo: scope,
310
+ expected: target
303
311
  });
304
312
  continue;
305
313
  }
@@ -320,7 +328,7 @@ function findingMessage(finding) {
320
328
  case "namespace_drifted": return `${head} the cortext coordinator is configured for namespace '${finding.configured}', but every kernel constraint and index is scoped to '${finding.scopedTo}'. So all of them are INACTIVE for '${finding.configured}': Todo/AgentMessage vocabularies unenforced, the mailbox inbox index covering no rows, and AgentIdentity.kid uniqueness NOT held. Writes succeed and queries answer, so nothing else will report this. Use '${finding.scopedTo}', or accept the loss deliberately.`;
321
329
  case "namespace_uncovered": return `${head} invariants are scoped to namespace '${finding.scopedTo}', which no loaded coordinator declares (declared: ${finding.declared.join(", ") || "none"}). Every kernel guarantee below is therefore INACTIVE, and nothing else will report it — a constraint scoped to a namespace with no rows passes trivially. ` + (finding.reason === "host_does_not_attribute" ? "This substrate does not attribute namespaces to their plugin (added in 5.1.0), so which namespace REPLACED it cannot be determined — upgrade the binary to get a named target rather than this list." : "This substrate does attribute, and the cortext coordinator declared no namespace at all — it is not loaded. The objects are orphaned rather than misscoped.") + " Set SYNOPTIC_CORTEXT_REPAIR=1 for one restart to re-scope.";
322
330
  case "object_missing": return `${head} ${finding.object.kind} '${finding.object.name}' (${finding.object.migration}) is ABSENT. Lost guarantee: ${finding.object.guarantee}. Migrations are recorded once by id, so this will NOT be reinstalled by a restart.`;
323
- case "object_misscoped": return `${head} ${finding.object.kind} '${finding.object.name}' is scoped to ${finding.scopedTo === null ? "no namespace" : `'${finding.scopedTo}'`}, not '${CORTEXT_NAMESPACE}'. Lost guarantee: ${finding.object.guarantee}. It exists, so nothing errors; it simply matches no rows.`;
331
+ case "object_misscoped": return `${head} ${finding.object.kind} '${finding.object.name}' is scoped to ${finding.scopedTo === null ? "no namespace" : `'${finding.scopedTo}'`}, not '${finding.expected}'. Lost guarantee: ${finding.object.guarantee}. It exists, so nothing errors; it simply matches no rows.`;
324
332
  case "object_not_validated": return `${head} ${finding.object.kind} '${finding.object.name}' exists but is NOT VALID — enforced for new writes only, and existing rows were never checked against it. Guarantee held only going forward: ${finding.object.guarantee}.`;
325
333
  }
326
334
  }
@@ -352,11 +360,14 @@ function findingMessage(finding) {
352
360
  * structured channel this belongs in.
353
361
  */
354
362
  async function reportInvariantAudit(ctx, warn = console.warn) {
355
- const findings = [...auditNamespaceCoverage(ctx.coordinatorNamespaces, ctx.coordinatorNamespacesByPlugin)];
363
+ let findings = [...auditNamespaceCoverage(ctx.coordinatorNamespaces, ctx.coordinatorNamespacesByPlugin)];
364
+ const target = ctx.coordinatorNamespaces.includes("cortext") ? CORTEXT_NAMESPACE : resolveCortextNamespaces(ctx.coordinatorNamespacesByPlugin)?.[0] ?? "cortext";
356
365
  const catalogRead = ctx.sql !== null && ctx.sql !== void 0;
357
366
  let objectsChecked = 0;
358
- if (catalogRead) try {
359
- const result = await auditObjects(ctx.sql);
367
+ const layout = catalogRead ? await storeLayout(ctx.sql).catch(() => "legacy") : null;
368
+ if (catalogRead && layout === "legacy") try {
369
+ const result = await auditObjects(ctx.sql, target);
370
+ if (target !== "cortext" && result.findings.length === 0) findings = findings.filter((f) => f.kind !== "namespace_drifted");
360
371
  findings.push(...result.findings);
361
372
  objectsChecked = result.checked;
362
373
  } catch (error) {
@@ -378,7 +389,8 @@ async function reportInvariantAudit(ctx, warn = console.warn) {
378
389
  ctx.metric("cortext_kernel.invariant_audit_complete", {
379
390
  checked: objectsChecked,
380
391
  findings: findings.length,
381
- catalogRead
392
+ catalogRead,
393
+ layout
382
394
  });
383
395
  return findings;
384
396
  }
@@ -505,24 +517,6 @@ async function rescopeVocabConstraint(s, metric, { constraint, type, column, voc
505
517
  return violations;
506
518
  }
507
519
  /**
508
- * Re-scope the mailbox inbox index (0003) to `namespace`.
509
- *
510
- * Dropped and rebuilt rather than left alongside a correctly-scoped sibling:
511
- * two partial indexes over the same expression is one the planner can never
512
- * use, plus write amplification on every AgentMessage insert.
513
- */
514
- async function rescopeRecipientIndex(s, namespace) {
515
- await s.unsafe("DROP INDEX IF EXISTS rows_agent_message_recipient_idx");
516
- await s.unsafe(`
517
- CREATE INDEX IF NOT EXISTS rows_agent_message_recipient_idx
518
- ON rows ((body->>'recipient'), status)
519
- WHERE namespace = '${namespace}'
520
- AND type = 'AgentMessage'
521
- AND deleted_at IS NULL
522
- AND archived IS NOT TRUE
523
- `);
524
- }
525
- /**
526
520
  * How many kids would collide if the unique index were scoped to `namespace`.
527
521
  *
528
522
  * Exposed separately because a UNIQUE index has no `NOT VALID` equivalent —
@@ -606,7 +600,8 @@ async function rescopeHostInstallKeyIndex(s, namespace) {
606
600
  //#region src/kernel/migrations/0001_baseline.ts
607
601
  const cortextBaselineMigration = {
608
602
  id: "0001_baseline",
609
- apply: async ({ sql, metric }) => {
603
+ apply: async (ctx) => {
604
+ const { sql, metric } = ctx;
610
605
  if (sql === null) return;
611
606
  const s = sql;
612
607
  await s.unsafe(`
@@ -616,22 +611,12 @@ const cortextBaselineMigration = {
616
611
  ADD COLUMN IF NOT EXISTS status text NULL,
617
612
  ADD COLUMN IF NOT EXISTS priority text NULL
618
613
  `);
619
- for (const column of [
620
- "archived",
621
- "project",
622
- "status",
623
- "priority"
624
- ]) await s.unsafe(`
625
- CREATE INDEX IF NOT EXISTS rows_ns_type_${column}_idx
626
- ON rows (namespace, type, ${column}) WHERE deleted_at IS NULL
627
- `);
614
+ if (ctx.layout === "partitioned") return;
628
615
  await s.unsafe(`
629
- CREATE INDEX IF NOT EXISTS rows_cold_start_idx
630
- ON rows (namespace, type, project)
631
- WHERE deleted_at IS NULL AND archived IS NOT TRUE
616
+ CREATE INDEX IF NOT EXISTS rows_ns_type_archived_idx
617
+ ON rows (namespace, type, archived) WHERE deleted_at IS NULL
632
618
  `);
633
619
  for (const spec of VOCAB_CONSTRAINTS) await rescopeVocabConstraint(s, metric, spec, CORTEXT_NAMESPACE);
634
- await rescopeRecipientIndex(s, CORTEXT_NAMESPACE);
635
620
  await rescopeKidUniqueIndex(s, CORTEXT_NAMESPACE);
636
621
  await rescopeHostInstallKeyIndex(s, CORTEXT_NAMESPACE);
637
622
  },
@@ -644,6 +629,24 @@ const cortextBaselineMigration = {
644
629
  }
645
630
  };
646
631
 
632
+ //#endregion
633
+ //#region src/kernel/migrations/0002_drop_engine_owned_indexes.ts
634
+ const ENGINE_OWNED_INDEXES = [
635
+ "rows_agent_message_recipient_idx",
636
+ "rows_ns_type_project_idx",
637
+ "rows_ns_type_status_idx",
638
+ "rows_ns_type_priority_idx",
639
+ "rows_cold_start_idx"
640
+ ];
641
+ const cortextDropEngineOwnedIndexesMigration = {
642
+ id: "0002_drop_engine_owned_indexes",
643
+ apply: async ({ sql, layout }) => {
644
+ if (sql === null || layout === "partitioned") return;
645
+ await sql.unsafe(`DROP INDEX IF EXISTS ${ENGINE_OWNED_INDEXES.join(", ")}`);
646
+ },
647
+ rollback: async () => {}
648
+ };
649
+
647
650
  //#endregion
648
651
  //#region src/kernel/repair.ts
649
652
  /** The env var that opts a deployment in, for one restart. */
@@ -681,11 +684,6 @@ async function repairInvariants(sql, metric, namespace) {
681
684
  object: spec.constraint
682
685
  });
683
686
  }
684
- await rescopeRecipientIndex(s, namespace);
685
- outcomes.push({
686
- kind: "repaired",
687
- object: "rows_agent_message_recipient_idx"
688
- });
689
687
  const duplicates = await countDuplicateKids(s, namespace);
690
688
  if (duplicates > 0) outcomes.push({
691
689
  kind: "skipped",
@@ -742,6 +740,11 @@ async function repairIfRequested(ctx, warn = console.warn) {
742
740
  ctx.metric("cortext_kernel.repair_declined", { reason: "target_unknown" });
743
741
  return null;
744
742
  }
743
+ if (await storeLayout(ctx.sql).catch(() => "legacy") === "partitioned") {
744
+ warn(`[cortext-kernel] ${REPAIR_ENV_VAR} is set, but rows is partitioned: the engine owns cortext's indexes there and there are no parent-level objects to repair. Nothing to do.`);
745
+ ctx.metric("cortext_kernel.repair_declined", { reason: "partitioned" });
746
+ return null;
747
+ }
745
748
  warn(`[cortext-kernel] ${REPAIR_ENV_VAR} is set — re-scoping kernel invariants to '${ctx.namespace}'. This takes ACCESS EXCLUSIVE on rows and briefly blocks writes.` + (ctx.namespace === "cortext" ? " Target matches the built-in scope literal, so this is a no-op rebuild." : ""));
746
749
  try {
747
750
  const outcomes = await repairInvariants(ctx.sql, ctx.metric, ctx.namespace);
@@ -786,7 +789,7 @@ async function repairIfRequested(ctx, warn = console.warn) {
786
789
  * by reflex: widen it only after reading what the new major changed.
787
790
  * `substrate-contract-parity.test.ts` holds the manifest to this literal.
788
791
  */
789
- const SUBSTRATE_CONTRACT_RANGE = ">=1.0.0-alpha.0 <2.0.0";
792
+ const SUBSTRATE_CONTRACT_RANGE = ">=1.0.0-alpha.10 <2.0.0";
790
793
 
791
794
  //#endregion
792
795
  //#region src/kernel/plugin.ts
@@ -796,6 +799,7 @@ function createCortextKernelPlugin() {
796
799
  contractVersion: SUBSTRATE_CONTRACT_RANGE,
797
800
  onKernelReady: async (ctx) => {
798
801
  await reportInvariantAudit(ctx);
802
+ if (!ctx.background) return;
799
803
  await repairIfRequested({
800
804
  sql: ctx.sql,
801
805
  namespace: resolveCortextNamespaces(ctx.coordinatorNamespacesByPlugin)?.[0] ?? null,
@@ -803,9 +807,9 @@ function createCortextKernelPlugin() {
803
807
  metric: ctx.metric
804
808
  });
805
809
  },
806
- migrations: [cortextBaselineMigration]
810
+ migrations: [cortextBaselineMigration, cortextDropEngineOwnedIndexesMigration]
807
811
  };
808
812
  }
809
813
 
810
814
  //#endregion
811
- export { CORTEXT_PROMOTED_COLUMNS as A, findingMessage as C, ARCHITECTURE_NAMESPACE as D, scopedNamespace as E, CORTEXT_COORDINATOR_NAME as O, auditObjects as S, resolveCortextNamespaces as T, TODO_LIFECYCLE_VOCAB as _, repairInvariants as a, INVARIANT_OBJECTS as b, cortextBaselineMigration as c, countDuplicateKids as d, rescopeHostInstallKeyIndex as f, AGENT_MESSAGE_STATUS_VOCAB as g, rescopeVocabConstraint as h, repairIfRequested as i, CORTEXT_NAMESPACE as k, VOCAB_CONSTRAINTS as l, rescopeRecipientIndex as m, SUBSTRATE_CONTRACT_RANGE as n, repairOutcomeMessage as o, rescopeKidUniqueIndex as p, REPAIR_ENV_VAR as r, repairRequested as s, createCortextKernelPlugin as t, countDuplicateHostInstallKeys as u, TODO_PRIORITY_VOCAB as v, reportInvariantAudit as w, auditNamespaceCoverage as x, TODO_RESOLUTION_VOCAB as y };
815
+ export { CORTEXT_NAMESPACE as A, auditObjects as C, scopedNamespace as D, resolveCortextNamespaces as E, ARCHITECTURE_NAMESPACE as O, auditNamespaceCoverage as S, reportInvariantAudit as T, AGENT_MESSAGE_STATUS_VOCAB as _, repairInvariants as a, TODO_RESOLUTION_VOCAB as b, ENGINE_OWNED_INDEXES as c, VOCAB_CONSTRAINTS as d, countDuplicateHostInstallKeys as f, rescopeVocabConstraint as g, rescopeKidUniqueIndex as h, repairIfRequested as i, CORTEXT_PROMOTED_COLUMNS as j, CORTEXT_COORDINATOR_NAME as k, cortextDropEngineOwnedIndexesMigration as l, rescopeHostInstallKeyIndex as m, SUBSTRATE_CONTRACT_RANGE as n, repairOutcomeMessage as o, countDuplicateKids as p, REPAIR_ENV_VAR as r, repairRequested as s, createCortextKernelPlugin as t, cortextBaselineMigration as u, TODO_LIFECYCLE_VOCAB as v, findingMessage as w, INVARIANT_OBJECTS as x, TODO_PRIORITY_VOCAB as y };
@@ -33,10 +33,9 @@
33
33
  * enum, or moving the substrate contract range are all breaks for someone holding
34
34
  * data or code against this schema.
35
35
  *
36
- * `indexed` is included and it is not cosmetic: un-indexing a field turns a
37
- * working `where` filter into an `unindexed_field` rejection. That is a
38
- * runtime break for a caller who changed nothing, and it would otherwise be
39
- * invisible to a version-bump decision.
36
+ * `indexed` is included as a review item, not a break: every field filters,
37
+ * so un-indexing one turns an index-served `where` into a scan of the type.
38
+ * The caller's query still works, but slower, and a human should weigh it.
40
39
  */
41
40
  /** One field, reduced to the properties a consumer can break against. */
42
41
  export interface SurfaceField {
@@ -1,5 +1,5 @@
1
- import { A as CORTEXT_PROMOTED_COLUMNS, D as ARCHITECTURE_NAMESPACE, O as CORTEXT_COORDINATOR_NAME, _ as TODO_LIFECYCLE_VOCAB, g as AGENT_MESSAGE_STATUS_VOCAB, k as CORTEXT_NAMESPACE, n as SUBSTRATE_CONTRACT_RANGE, v as TODO_PRIORITY_VOCAB, y as TODO_RESOLUTION_VOCAB } from "./plugin-27UpMnoE.js";
2
- import { JSON_HEADERS, RESERVED_AUTOCRUD_FIELDS, ResolverFrameError, and, applyUpdatePatch, createResolverWithEdges, defineNamespace, deleteResolver, denyAll, denyVerbs, field, formatQualifiedKey, getResolver, grantOf, grantedIn, jsonResponse, listResolver, makeCtx, or, queryAll, requireIdentity, resolveEntityRow, resolveIdentity, selectAll, updateResolver } from "@mnmal-ai/substrate/sdk";
1
+ import { A as CORTEXT_NAMESPACE, O as ARCHITECTURE_NAMESPACE, _ as AGENT_MESSAGE_STATUS_VOCAB, b as TODO_RESOLUTION_VOCAB, j as CORTEXT_PROMOTED_COLUMNS, k as CORTEXT_COORDINATOR_NAME, n as SUBSTRATE_CONTRACT_RANGE, v as TODO_LIFECYCLE_VOCAB, y as TODO_PRIORITY_VOCAB } from "./plugin-CujP_pvT.js";
2
+ import { JSON_HEADERS, RESERVED_AUTOCRUD_FIELDS, ResolverFrameError, and, createResolverWithEdges, defineNamespace, deleteResolver, denyAll, denyVerbs, field, formatQualifiedKey, getResolver, grantOf, grantedIn, jsonResponse, listResolver, makeCtx, or, queryAll, requireIdentity, resolveEntityRow, resolveIdentity, selectAll, updateResolver } from "@mnmal-ai/substrate/sdk";
3
3
 
4
4
  //#region src/auth-policy.ts
5
5
  /**
@@ -163,8 +163,12 @@ function parseBody(req) {
163
163
  } catch (err) {
164
164
  return { error: err instanceof Error ? err.message : "invalid JSON" };
165
165
  }
166
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return { dryRun: false };
167
- return { dryRun: parsed.dryRun === true };
166
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return { error: "body must be a JSON object, e.g. {\"dryRun\": true}" };
167
+ const unknown = Object.keys(parsed).filter((k) => k !== "dryRun");
168
+ if (unknown.length > 0) return { error: `unknown field(s): ${unknown.join(", ")}` };
169
+ const dryRun = parsed.dryRun;
170
+ if (dryRun !== void 0 && typeof dryRun !== "boolean") return { error: "\"dryRun\" must be true or false" };
171
+ return { dryRun: dryRun === true };
168
172
  }
169
173
  function registerCompactRoute(router, ctx, retention, namespace, auth = DEFAULT_AUTH) {
170
174
  router.post("/compact", async (req) => {
@@ -295,10 +299,44 @@ function messageParticipantsOnly(installCtx, name, inner) {
295
299
  };
296
300
  }
297
301
  /**
302
+ * Whether `host` names the machine in `kid`'s `@host` part, ignoring case.
303
+ * A kid with no `@` names no machine, so it may carry no host.
304
+ */
305
+ function hostMatchesKid(host, kid) {
306
+ if (host === void 0 || host === null) return true;
307
+ if (typeof kid !== "string" || !kid.includes("@")) return false;
308
+ return String(host).toLowerCase() === kid.slice(kid.lastIndexOf("@") + 1).toLowerCase();
309
+ }
310
+ function refuseForeignHost(name, host, kid) {
311
+ if (hostMatchesKid(host, kid)) return;
312
+ throw new ResolverFrameError("operator_only", `${name}: 'host' must be the machine in the kid ('${String(kid)}'); only an operator may set another`, { field: "host" });
313
+ }
314
+ /**
315
+ * AgentIdentity create: a non-operator may not file a row whose `host` is a
316
+ * machine other than the one its kid names. The fleet audit trusts `host`
317
+ * (and `roaming` on that host) when it decides who may write a host's
318
+ * HostInstall rows.
319
+ */
320
+ function identityCreateGuard(installCtx, name, inner) {
321
+ return async (ctx, args) => {
322
+ const params = args.params ?? {};
323
+ if (!isOperator(ctx)) refuseForeignHost(name, params.host, params.kid);
324
+ if (typeof params.kid === "string") {
325
+ const { rows } = await (args.dataset ?? installCtx.dataset).read("AgentIdentity", {
326
+ where: { kid: { eq: params.kid } },
327
+ limit: 1
328
+ });
329
+ if (rows.length > 0) throw new ResolverFrameError("duplicate_kid", `${name}: an AgentIdentity row for '${params.kid}' already exists; update it instead`, { field: "kid" });
330
+ }
331
+ return inner(ctx, args);
332
+ };
333
+ }
334
+ /**
298
335
  * AgentIdentity update: the identity itself (the caller's kid is the row's
299
336
  * kid) or an operator. `kid` is the row's identity, so changing it is an
300
337
  * operator act even on your own row: otherwise a row could be renamed onto a
301
- * kid whose mail it would then receive.
338
+ * kid whose mail it would then receive. `host` must stay the kid's machine,
339
+ * and reactivating a retired row is an operator act.
302
340
  */
303
341
  function identitySelfOnly(installCtx, name, inner) {
304
342
  return async (ctx, args) => {
@@ -309,10 +347,45 @@ function identitySelfOnly(installCtx, name, inner) {
309
347
  if (row === null) return inner(ctx, args);
310
348
  const params = args.params ?? {};
311
349
  if ("kid" in params && params.kid !== row.kid) throw new ResolverFrameError("operator_only", `${name}: 'kid' is the row's identity and only an operator may change it`, { field: "kid" });
312
- if (callerKid(ctx) === row.kid) return inner(ctx, args);
350
+ if (callerKid(ctx) === row.kid) {
351
+ if ("host" in params || "roaming" in params || "project" in params) refuseForeignHost(name, "host" in params ? params.host : row.host, row.kid);
352
+ if (params.active === true && row.active === false) throw new ResolverFrameError("operator_only", `${name}: a retired identity may only be reactivated by an operator`, { field: "active" });
353
+ return inner(ctx, args);
354
+ }
313
355
  throw new ResolverFrameError("not_self", `${name}: only '${String(row.kid)}' itself, or an operator, may edit this identity row`, { id: row.id });
314
356
  };
315
357
  }
358
+ /**
359
+ * Todo create/update: `resolution` says why a CLOSED Todo stopped, so a live
360
+ * Todo carries none. A resolution with a live resulting status is refused,
361
+ * and moving a Todo back to a live status clears the one it had.
362
+ */
363
+ function todoResolutionGuard(installCtx, name, inner, statusAliases) {
364
+ const canon = (v) => {
365
+ const s = String(v).trim().toLowerCase();
366
+ return statusAliases[s] ?? s;
367
+ };
368
+ return async (ctx, args) => {
369
+ const params = { ...args.params ?? {} };
370
+ const statusGiven = params.status !== void 0 && params.status !== null;
371
+ let resulting = statusGiven ? canon(params.status) : null;
372
+ let current = null;
373
+ const id = rowId(args);
374
+ if (id !== void 0) {
375
+ const ds = args.dataset ?? installCtx.dataset;
376
+ current = await resolveEntityRow(ds, "Todo", id);
377
+ if (resulting === null && current !== null) resulting = canon(current.status);
378
+ }
379
+ if (resulting === null) resulting = "open";
380
+ if (resulting === "closed") return inner(ctx, args);
381
+ if (params.resolution !== void 0 && params.resolution !== null) throw new ResolverFrameError("resolution_on_live_todo", `${name}: 'resolution' is for closed Todos; this one would be '${resulting}'`, { field: "resolution" });
382
+ if (statusGiven && current !== null && current.resolution != null && !("resolution" in params)) params.resolution = null;
383
+ return inner(ctx, {
384
+ ...args,
385
+ params
386
+ });
387
+ };
388
+ }
316
389
  /** AgentIdentity delete: operators only. Mail hangs off these rows. */
317
390
  function operatorOnly(name, inner) {
318
391
  return async (ctx, args) => {
@@ -541,12 +614,27 @@ function transitionResolver(installCtx, target) {
541
614
  if (identityRow === void 0) throw new ResolverFrameError("no_recipient", `${name}: the recipient identity row no longer exists`, { id: message.id });
542
615
  const actor = resolveIdentity(ctx);
543
616
  if (actor.id !== identityRow.kid) throw new ResolverFrameError("not_recipient", `${name}: only the recipient ('${String(identityRow.kid)}') may transition this message`, { recipient: identityRow.kid });
544
- if (isNoOp(message.status)) return message;
545
- const at = ctx.now.toISOString();
546
- return applyUpdatePatch(ds, TYPE, message.id, {
547
- status: target,
548
- ...stamp(actor, at)
549
- }, ctx, args.onWrite);
617
+ let current = message;
618
+ for (let attempt = 0; attempt < 3 && current !== null; attempt++) {
619
+ if (isNoOp(current.status)) return current;
620
+ const { matched, row } = await ds.conditionalUpdate(TYPE, current.id, { status: current.status }, {
621
+ status: target,
622
+ ...stamp(actor, ctx.now.toISOString())
623
+ }, actor, ctx.now);
624
+ if (matched && row !== null) {
625
+ args.onWrite?.({
626
+ ns: ds.namespace,
627
+ type: TYPE,
628
+ id: row.id,
629
+ operation: "update",
630
+ row
631
+ });
632
+ return row;
633
+ }
634
+ current = await resolveEntityRow(ds, TYPE, message.id);
635
+ }
636
+ if (current === null) return null;
637
+ throw new ResolverFrameError("conflict", `${name}: the message kept changing while it was being transitioned; retry`, { id: message.id });
550
638
  };
551
639
  }
552
640
  /**
@@ -989,7 +1077,8 @@ function buildClaudeNamespace(ns) {
989
1077
  area: field.string(),
990
1078
  status: field.vocab(TODO_LIFECYCLE_VOCAB, {
991
1079
  aliases: TODO_STATUS_ALIASES,
992
- implies: TODO_STATUS_IMPLIES
1080
+ implies: TODO_STATUS_IMPLIES,
1081
+ indexed: true
993
1082
  }),
994
1083
  resolution: field.vocab(TODO_RESOLUTION_VOCAB, {
995
1084
  aliases: TODO_RESOLUTION_ALIASES,
@@ -1026,7 +1115,10 @@ function buildClaudeNamespace(ns) {
1026
1115
  nullable: true,
1027
1116
  onDelete: "set_null"
1028
1117
  }),
1029
- project: field.string({ nullable: true })
1118
+ project: field.string({
1119
+ nullable: true,
1120
+ indexed: true
1121
+ })
1030
1122
  } },
1031
1123
  AgentRule: {
1032
1124
  fields: {
@@ -1045,7 +1137,10 @@ function buildClaudeNamespace(ns) {
1045
1137
  onDelete: "set_null"
1046
1138
  }),
1047
1139
  archived: field.boolean({ default: false }),
1048
- project: field.string({ nullable: true })
1140
+ project: field.string({
1141
+ nullable: true,
1142
+ indexed: true
1143
+ })
1049
1144
  },
1050
1145
  embed: { source: (r) => {
1051
1146
  return `${embedText(r.synopsis, typeof r.rule === "string" ? r.rule : "")}${typeof r.category === "string" && r.category.length > 0 ? ` [${r.category}]` : ""}`;
@@ -1098,7 +1193,10 @@ function buildClaudeNamespace(ns) {
1098
1193
  body: field.string({ indexed: false }),
1099
1194
  synopsis: field.string({ nullable: true }),
1100
1195
  archived: field.boolean({ default: false }),
1101
- project: field.string({ nullable: true })
1196
+ project: field.string({
1197
+ nullable: true,
1198
+ indexed: true
1199
+ })
1102
1200
  },
1103
1201
  embed: { source: (r) => {
1104
1202
  const desc = typeof r.description === "string" ? r.description : "";
@@ -1115,7 +1213,10 @@ function buildClaudeNamespace(ns) {
1115
1213
  indexed: false
1116
1214
  }),
1117
1215
  authoredBy: field.struct({ kind: field.string() }),
1118
- project: field.string({ nullable: true }),
1216
+ project: field.string({
1217
+ nullable: true,
1218
+ indexed: true
1219
+ }),
1119
1220
  archived: field.boolean({ default: false })
1120
1221
  },
1121
1222
  embed: { source: (r) => embedText(r.synopsis, typeof r.body === "string" ? r.body : "") },
@@ -1157,19 +1258,27 @@ function buildClaudeNamespace(ns) {
1157
1258
  version: field.string(),
1158
1259
  active: field.boolean()
1159
1260
  } },
1160
- AgentIdentity: { fields: {
1161
- kid: field.string(),
1162
- project: field.string({ nullable: true }),
1163
- host: field.string(),
1164
- displayName: field.string({ nullable: true }),
1165
- description: field.string({
1166
- nullable: true,
1167
- indexed: false
1168
- }),
1169
- active: field.boolean({ default: true }),
1170
- broadcastEligible: field.boolean({ default: true }),
1171
- roaming: field.boolean({ default: false })
1172
- } },
1261
+ AgentIdentity: {
1262
+ fields: {
1263
+ kid: field.string(),
1264
+ project: field.string({ nullable: true }),
1265
+ host: field.string(),
1266
+ displayName: field.string({ nullable: true }),
1267
+ description: field.string({
1268
+ nullable: true,
1269
+ indexed: false
1270
+ }),
1271
+ active: field.boolean({ default: true }),
1272
+ broadcastEligible: field.boolean({ default: true }),
1273
+ roaming: field.boolean({ default: false })
1274
+ },
1275
+ indexes: [{
1276
+ fields: ["kid"],
1277
+ unique: true,
1278
+ where: "live",
1279
+ name: "rows_agent_identity_kid_unique"
1280
+ }]
1281
+ },
1173
1282
  AgentMessage: {
1174
1283
  autoCrud: false,
1175
1284
  fields: {
@@ -1199,40 +1308,48 @@ function buildClaudeNamespace(ns) {
1199
1308
  archived: field.boolean({ default: false })
1200
1309
  }
1201
1310
  },
1202
- HostInstall: { fields: {
1203
- key: field.string(),
1204
- host: field.string(),
1205
- hostReported: field.string(),
1206
- project: field.string(),
1207
- kid: field.string({ nullable: true }),
1208
- harness: field.string(),
1209
- pluginVersion: field.string({ nullable: true }),
1210
- fragmentVersion: field.string({ nullable: true }),
1211
- fragmentDigest: field.string({ nullable: true }),
1212
- fragmentState: field.enum(HOST_INSTALL_FRAGMENT_STATES),
1213
- refreshedAt: field.date(),
1214
- pinState: field.enum(HOST_INSTALL_CHECK_STATES, { nullable: true }),
1215
- pinNote: field.string({
1216
- nullable: true,
1217
- indexed: false
1218
- }),
1219
- mcpJsonState: field.enum(HOST_INSTALL_CHECK_STATES, { nullable: true }),
1220
- mcpJsonNote: field.string({
1221
- nullable: true,
1222
- indexed: false
1223
- }),
1224
- authorityState: field.enum(HOST_INSTALL_CHECK_STATES, { nullable: true }),
1225
- authorityNote: field.string({
1226
- nullable: true,
1227
- indexed: false
1228
- }),
1229
- settingsState: field.enum(HOST_INSTALL_CHECK_STATES, { nullable: true }),
1230
- settingsNote: field.string({
1231
- nullable: true,
1232
- indexed: false
1233
- }),
1234
- settingsEnv: field.json({ nullable: true })
1235
- } }
1311
+ HostInstall: {
1312
+ fields: {
1313
+ key: field.string(),
1314
+ host: field.string(),
1315
+ hostReported: field.string(),
1316
+ project: field.string(),
1317
+ kid: field.string({ nullable: true }),
1318
+ harness: field.string(),
1319
+ pluginVersion: field.string({ nullable: true }),
1320
+ fragmentVersion: field.string({ nullable: true }),
1321
+ fragmentDigest: field.string({ nullable: true }),
1322
+ fragmentState: field.enum(HOST_INSTALL_FRAGMENT_STATES),
1323
+ refreshedAt: field.date(),
1324
+ pinState: field.enum(HOST_INSTALL_CHECK_STATES, { nullable: true }),
1325
+ pinNote: field.string({
1326
+ nullable: true,
1327
+ indexed: false
1328
+ }),
1329
+ mcpJsonState: field.enum(HOST_INSTALL_CHECK_STATES, { nullable: true }),
1330
+ mcpJsonNote: field.string({
1331
+ nullable: true,
1332
+ indexed: false
1333
+ }),
1334
+ authorityState: field.enum(HOST_INSTALL_CHECK_STATES, { nullable: true }),
1335
+ authorityNote: field.string({
1336
+ nullable: true,
1337
+ indexed: false
1338
+ }),
1339
+ settingsState: field.enum(HOST_INSTALL_CHECK_STATES, { nullable: true }),
1340
+ settingsNote: field.string({
1341
+ nullable: true,
1342
+ indexed: false
1343
+ }),
1344
+ settingsEnv: field.json({ nullable: true })
1345
+ },
1346
+ indexes: [{
1347
+ fields: ["key"],
1348
+ unique: true,
1349
+ where: "live",
1350
+ name: "rows_host_install_key_unique"
1351
+ }]
1352
+ }
1236
1353
  };
1237
1354
  const definition = defineNamespace(ns, types, {
1238
1355
  annotations: { nl: CORTEXT_NL_RULINGS },
@@ -1244,7 +1361,13 @@ function buildClaudeNamespace(ns) {
1244
1361
  install(ctx) {
1245
1362
  baseInstall(ctx);
1246
1363
  installMailboxMutations(ctx, definition.types.AgentMessage.fields);
1247
- for (const [name, wrap] of [["updateAgentIdentity", (r) => identitySelfOnly(ctx, "updateAgentIdentity", r)], ["deleteAgentIdentity", (r) => operatorOnly("deleteAgentIdentity", r)]]) {
1364
+ for (const [name, wrap] of [
1365
+ ["createAgentIdentity", (r) => identityCreateGuard(ctx, "createAgentIdentity", r)],
1366
+ ["updateAgentIdentity", (r) => identitySelfOnly(ctx, "updateAgentIdentity", r)],
1367
+ ["deleteAgentIdentity", (r) => operatorOnly("deleteAgentIdentity", r)],
1368
+ ["createTodo", (r) => todoResolutionGuard(ctx, "createTodo", r, TODO_STATUS_ALIASES)],
1369
+ ["updateTodo", (r) => todoResolutionGuard(ctx, "updateTodo", r, TODO_STATUS_ALIASES)]
1370
+ ]) {
1248
1371
  const entry = ctx.registry.get(name);
1249
1372
  if (entry === void 0) throw new Error(`cortext: autoCRUD did not register ${name}`);
1250
1373
  ctx.registry.replace(name, {
@@ -1592,7 +1715,11 @@ function buildBody(namespace, retention) {
1592
1715
  description: "Load recent context at the start of each session",
1593
1716
  query: {
1594
1717
  SessionLog: {
1595
- params: { limit: 3 },
1718
+ params: {
1719
+ where: { archived: { eq: false } },
1720
+ orderBy: [{ createdAt: "desc" }],
1721
+ limit: 3
1722
+ },
1596
1723
  fields: {
1597
1724
  id: true,
1598
1725
  branch: true,
@@ -1602,7 +1729,11 @@ function buildBody(namespace, retention) {
1602
1729
  }
1603
1730
  },
1604
1731
  Decision: {
1605
- params: { limit: 80 },
1732
+ params: {
1733
+ where: { archived: { eq: false } },
1734
+ orderBy: [{ createdAt: "desc" }],
1735
+ limit: 80
1736
+ },
1606
1737
  fields: {
1607
1738
  id: true,
1608
1739
  topic: true,
@@ -1615,7 +1746,18 @@ function buildBody(namespace, retention) {
1615
1746
  }
1616
1747
  },
1617
1748
  Todo: {
1618
- params: { limit: 50 },
1749
+ params: {
1750
+ where: {
1751
+ archived: { eq: false },
1752
+ status: { in: [
1753
+ "open",
1754
+ "in_progress",
1755
+ "blocked"
1756
+ ] }
1757
+ },
1758
+ orderBy: [{ createdAt: "desc" }],
1759
+ limit: 50
1760
+ },
1619
1761
  fields: {
1620
1762
  id: true,
1621
1763
  title: true,
@@ -1631,7 +1773,7 @@ function buildBody(namespace, retention) {
1631
1773
  surfaceToUser: [
1632
1774
  "The 3 most recent non-archived SessionLog entries",
1633
1775
  "Non-archived Decisions",
1634
- "Todos with status 'open' or 'blocked'",
1776
+ "Todos with status 'open', 'in_progress' or 'blocked'",
1635
1777
  "Skip archived records silently"
1636
1778
  ]
1637
1779
  } },
@@ -1697,10 +1839,9 @@ function registerOnboardRoute(router, _ctx, retention, namespace) {
1697
1839
  * enum, or moving the substrate contract range are all breaks for someone holding
1698
1840
  * data or code against this schema.
1699
1841
  *
1700
- * `indexed` is included and it is not cosmetic: un-indexing a field turns a
1701
- * working `where` filter into an `unindexed_field` rejection. That is a
1702
- * runtime break for a caller who changed nothing, and it would otherwise be
1703
- * invisible to a version-bump decision.
1842
+ * `indexed` is included as a review item, not a break: every field filters,
1843
+ * so un-indexing one turns an index-served `where` into a scan of the type.
1844
+ * The caller's query still works, but slower, and a human should weigh it.
1704
1845
  */
1705
1846
  function reduceField(raw) {
1706
1847
  const nullable = raw.nullable === true;
@@ -1791,7 +1932,7 @@ function classifySurfaceChange(before, after) {
1791
1932
  if (!bf.required && af.required) breaking.push(`field became REQUIRED: ${at}`);
1792
1933
  if (bf.required && !af.required) additive.push(`field relaxed to optional: ${at}`);
1793
1934
  if (bf.nullable && !af.nullable) breaking.push(`field no longer nullable: ${at}`);
1794
- if (bf.indexed && !af.indexed) breaking.push(`field no longer indexed: ${at}`);
1935
+ if (bf.indexed && !af.indexed) review.push(`field no longer indexed: ${at}`);
1795
1936
  if (!bf.indexed && af.indexed) additive.push(`field now indexed: ${at}`);
1796
1937
  const bEnum = bf.enum ?? null;
1797
1938
  const aEnum = af.enum ?? null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnmal-ai/cortext",
3
- "version": "1.0.0-alpha.1",
3
+ "version": "1.0.0-alpha.10",
4
4
  "description": "Cortext: the agent-neutral coordination context store for Substrate (Todos, Decisions, SessionLogs, AgentRules, Memory, Concepts, Councils, …) plus architecture findings. One package carrying two plugins: the coordinator that registers the namespaces, and the kernel plugin that owns their Postgres invariants.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -62,10 +62,10 @@
62
62
  "zod": "^4.6.5"
63
63
  },
64
64
  "peerDependencies": {
65
- "@mnmal-ai/substrate": ">=1.0.0-alpha.0 <2.0.0"
65
+ "@mnmal-ai/substrate": ">=1.0.0-alpha.10 <2.0.0"
66
66
  },
67
67
  "devDependencies": {
68
- "@mnmal-ai/substrate": "1.0.0-alpha.0",
68
+ "@mnmal-ai/substrate": "1.0.0-alpha.10",
69
69
  "@types/node": "^26.6.4",
70
70
  "tsdown": "^0.23.0",
71
71
  "typescript": "^7.0.2",