@securecoms/agent-bridge 0.28.1 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +192 -79
- package/dist/bin.js +2841 -169
- package/dist/bridge-agent.d.ts +190 -1
- package/dist/config.d.ts +2 -0
- package/dist/embedder.js +2 -1
- package/dist/index.js +2815 -143
- package/dist/memory/embedder/client.d.ts +20 -0
- package/dist/memory/embedder/protocol.d.ts +5 -0
- package/dist/memory/note-frames.d.ts +1 -1
- package/dist/memory/recall/recall-book.d.ts +12 -2
- package/dist/memory/unified/chunk.d.ts +12 -0
- package/dist/memory/unified/config.d.ts +7 -0
- package/dist/memory/unified/consumer-unified.d.ts +21 -0
- package/dist/memory/unified/escape.d.ts +23 -0
- package/dist/memory/unified/index-key.d.ts +8 -0
- package/dist/memory/unified/item-book.d.ts +70 -0
- package/dist/memory/unified/item-reembed.d.ts +20 -0
- package/dist/memory/unified/migrate.d.ts +68 -0
- package/dist/memory/unified/owner-scope.d.ts +48 -0
- package/dist/memory/unified/pin-rules.d.ts +18 -0
- package/dist/memory/unified/ranking-client.d.ts +48 -0
- package/dist/memory/unified/ranking-core-constants.d.ts +8 -0
- package/dist/memory/unified/ranking-core.d.ts +117 -0
- package/dist/memory/unified/ranking-inprocess.d.ts +3 -0
- package/dist/memory/unified/ranking-worker.d.ts +1 -0
- package/dist/memory/unified/read-book.d.ts +80 -0
- package/dist/memory/unified/switch.d.ts +8 -0
- package/dist/memory/unified/text.d.ts +15 -0
- package/dist/memory/unified/worker-protocol.d.ts +102 -0
- package/dist/memory-worker.js +583 -0
- package/dist/version.d.ts +1 -1
- package/dist/ws-server.d.ts +3 -1
- package/package.json +1 -1
|
@@ -16,6 +16,12 @@ export interface Embedder {
|
|
|
16
16
|
state(): EmbedderState;
|
|
17
17
|
ensureStarted(): void;
|
|
18
18
|
filesPresent(): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Cold-path ruling (2026-10-09): true at once when ready; while preparing, true on the transition to ready and false
|
|
21
|
+
* on a failure, a stop or after `ms`. False at once in any other state. Never rejects and never starts, connects or
|
|
22
|
+
* spawns anything itself (the caller runs ensureStarted first).
|
|
23
|
+
*/
|
|
24
|
+
ready(ms: number): Promise<boolean>;
|
|
19
25
|
embed(text: string, priority: 0 | 1, opts?: {
|
|
20
26
|
signal?: AbortSignal;
|
|
21
27
|
}): Promise<Float32Array>;
|
|
@@ -64,12 +70,19 @@ export declare class EmbedderClient implements Embedder {
|
|
|
64
70
|
private verifyAttempt;
|
|
65
71
|
private fails;
|
|
66
72
|
private closed;
|
|
73
|
+
/** When this client last sent a job (or connected, if later): the idle-stop test (cold-path ruling 2026-10-09). */
|
|
74
|
+
private lastActivity;
|
|
75
|
+
/** ready(ms) waiters: settled on the next transition out of "preparing". */
|
|
76
|
+
private readonly readyWaiters;
|
|
67
77
|
private readonly m;
|
|
68
78
|
private readonly now;
|
|
69
79
|
private readonly log;
|
|
70
80
|
constructor(o: EmbedderClientOptions);
|
|
71
81
|
/** Pure: the report path reads this, and reading it never starts, connects or spawns anything (plan audit LOW). */
|
|
72
82
|
state(): EmbedderState;
|
|
83
|
+
/** Cold-path ruling (2026-10-09): see Embedder.ready. Pure like state(): never starts, connects or spawns anything. */
|
|
84
|
+
ready(ms: number): Promise<boolean>;
|
|
85
|
+
private settleReady;
|
|
73
86
|
private tcp;
|
|
74
87
|
filesPresent(): boolean;
|
|
75
88
|
private key;
|
|
@@ -87,6 +100,13 @@ export declare class EmbedderClient implements Embedder {
|
|
|
87
100
|
private aclToken;
|
|
88
101
|
private aclOk;
|
|
89
102
|
private fail;
|
|
103
|
+
/**
|
|
104
|
+
* Rule 6, cold-path ruling (2026-10-09): the served socket closed. With no job pending and this client's last job (or
|
|
105
|
+
* its connect, if later) at least the shared idle constant ago, it is the shared embedder's idle stop, which is not a
|
|
106
|
+
* failure: back to `off`, no `fails++`, no `notBefore`, no unavailable report, so the next turn's ensureStarted starts
|
|
107
|
+
* it at once. Anything else (a job pending, or recent use) stays a failure with its backoff.
|
|
108
|
+
*/
|
|
109
|
+
private onServedClose;
|
|
90
110
|
ensureStarted(): void;
|
|
91
111
|
private start;
|
|
92
112
|
/** The embedder's fixed-code status file (exit 3). True when it decided the state. */
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/** Spec §20 E1.2 to E1.4: newline-delimited JSON frames over the local socket; vectors as base64 little-endian float32. */
|
|
2
2
|
export declare const EMBEDDER_PROTOCOL = 1;
|
|
3
3
|
export declare const MAX_TEXT_BYTES = 8192;
|
|
4
|
+
/**
|
|
5
|
+
* The shared embedder stops after this long with no job from any bridge (server `idleMs` default). The client uses the
|
|
6
|
+
* same value to tell that idle stop from a failure (cold-path ruling 2026-10-09): ONE constant, used by both.
|
|
7
|
+
*/
|
|
8
|
+
export declare const EMBEDDER_IDLE_MS: number;
|
|
4
9
|
/**
|
|
5
10
|
* 11b review M4: the longest sealed job line. Worst case for MAX_TEXT_BYTES of text: every byte a control character that
|
|
6
11
|
* JSON escapes as \u00XX (6 bytes) = 49152, plus the frame's other fields (< 100), plus the 24-byte nonce and 16-byte
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* is ignored.
|
|
9
9
|
*/
|
|
10
10
|
/** Spec §8.2, §18.1: content-free refusal classes, the only thing a refusal says. */
|
|
11
|
-
export declare const REASON_CLASSES: readonly ["mode", "invalid", "message", "audience", "provenance", "secret", "cap", "cleared", "store", "budget", "purged"];
|
|
11
|
+
export declare const REASON_CLASSES: readonly ["mode", "invalid", "message", "audience", "provenance", "secret", "cap", "cleared", "store", "budget", "purged", "not_owner_words"];
|
|
12
12
|
export type ReasonClass = (typeof REASON_CLASSES)[number];
|
|
13
13
|
export interface MemoryNoteFrame {
|
|
14
14
|
request_id: string;
|
|
@@ -62,8 +62,11 @@ export declare class RecallBook {
|
|
|
62
62
|
private static readonly IDLE_MS;
|
|
63
63
|
private static readonly TOP_K;
|
|
64
64
|
private static readonly TEXT_MAX_CHARS;
|
|
65
|
+
/** About 1.5 ms of real opens at the measured ~40 µs per row: the longest a warm holds the event loop. */
|
|
66
|
+
private static readonly WARM_YIELD_ROWS;
|
|
65
67
|
private readonly caches;
|
|
66
68
|
private readonly entries;
|
|
69
|
+
/** The warm in flight per conversation: a cold turn awaits the same warm instead of starting a second one. */
|
|
67
70
|
private readonly warming;
|
|
68
71
|
private same;
|
|
69
72
|
private nowMs;
|
|
@@ -89,12 +92,18 @@ export declare class RecallBook {
|
|
|
89
92
|
* re-read capture.
|
|
90
93
|
*/
|
|
91
94
|
warm(conversationId: string): Promise<void>;
|
|
95
|
+
private warmOnce;
|
|
92
96
|
/**
|
|
93
97
|
* §7.1: compute at ingest. Snapshot the capture FIRST (H2); `receipt`
|
|
94
98
|
* replaces its purge and mode epochs with the ones captured when the turn
|
|
95
99
|
* was received (Task 16 fix H-2), so a purge or a mode change after receipt
|
|
96
|
-
* refuses too.
|
|
97
|
-
*
|
|
100
|
+
* refuses too.
|
|
101
|
+
*
|
|
102
|
+
* Cold-path ruling (architect, 2026-10-09; §7.1 and §7.3 errata): ONE deadline, start + `timeoutMs` (the caller
|
|
103
|
+
* passes its turn kind's bound: 600 ms text, 250 ms voice), covers the ready wait, the warm and the query embed
|
|
104
|
+
* together; no phase has a budget of its own. A cold embedder is started (ensureStarted) and waited for; a cold or
|
|
105
|
+
* stale cache is warmed and waited for. Past the deadline there is no recall and nothing attaches late: a warm that
|
|
106
|
+
* outlives it keeps running and may install (warm enforces H2 itself), but this turn gets no entry.
|
|
98
107
|
*/
|
|
99
108
|
prepare(p: {
|
|
100
109
|
messageId: string;
|
|
@@ -107,6 +116,7 @@ export declare class RecallBook {
|
|
|
107
116
|
modeEpoch: number;
|
|
108
117
|
};
|
|
109
118
|
}): Promise<void>;
|
|
119
|
+
private attach;
|
|
110
120
|
/** Q3 + Q6: the entry's own conversation, and its capture still current at delivery. Synchronous (ws flush). */
|
|
111
121
|
deliverable(messageId: string, conversationId: string): readonly RecallItem[] | undefined;
|
|
112
122
|
/**
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const CHUNK_TARGET = 600;
|
|
2
|
+
export declare const CHUNK_CAP = 800;
|
|
3
|
+
export declare const CHUNK_OVERLAP = 100;
|
|
4
|
+
export declare function chunkText(text: string): string[];
|
|
5
|
+
/** M-2 then R-U15, in that order: the whole text is scanned once; any hit drops the whole message. */
|
|
6
|
+
export declare function prepareMessageText(text: string): {
|
|
7
|
+
ok: true;
|
|
8
|
+
chunks: string[];
|
|
9
|
+
} | {
|
|
10
|
+
ok: false;
|
|
11
|
+
reason: "secret" | "empty";
|
|
12
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec section 8.1 step 8, U6: unified memory attaches at delivery to the frame copy only, for a runtime socket that
|
|
3
|
+
* declared memory_unified and never claimed a harness. The buffer, the sealed runtime_inbox and `inflight` keep the
|
|
4
|
+
* original, so a re-buffered message carries none to an HTTP or harness consumer. The owner predicate and the owner scope
|
|
5
|
+
* are re-checked in the source (`BridgeAgent.unifiedForDelivery`) on the ORIGINAL message's frozen authorization (H-4).
|
|
6
|
+
*/
|
|
7
|
+
import type { BridgeMessage } from "../../bridge-agent.js";
|
|
8
|
+
import type { UnifiedFrameItem } from "./read-book.js";
|
|
9
|
+
export declare function withConsumerUnified(out: BridgeMessage, original: BridgeMessage, session: {
|
|
10
|
+
consumer: "runtime" | "unknown";
|
|
11
|
+
harnessClaimed?: boolean;
|
|
12
|
+
unifiedConsumer: boolean;
|
|
13
|
+
}, src: {
|
|
14
|
+
unifiedForDelivery(m: BridgeMessage): {
|
|
15
|
+
items: UnifiedFrameItem[];
|
|
16
|
+
pinnedMore: number;
|
|
17
|
+
} | undefined;
|
|
18
|
+
}): {
|
|
19
|
+
message: BridgeMessage;
|
|
20
|
+
shown: ReadonlySet<number>;
|
|
21
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified memory (spec \u00A710.3, R-U26, M-4, C-1): the escape the bridge applies to the text it quotes in its own lines
|
|
3
|
+
* (`Pinned:`, `Forgot:`). The runtime renders every block with its own copy of the same function (parity, plan Task 16),
|
|
4
|
+
* so this file is self-contained. Line breaks and other control characters become a space (the runtime's CONTROL_CHARS
|
|
5
|
+
* rule, memory.ts:16) so a quoted text can never open a new line; then the remaining C-1 classes are stripped; then the
|
|
6
|
+
* text is capped to `max` code points; only then are `"` and `\` backslash-escaped (ruling C21). Capping before the
|
|
7
|
+
* escape means every backslash in the output comes from a whole pair or a whole `\"`, so the output never ends in an
|
|
8
|
+
* unpaired backslash that would escape the closing quote. The escaped output can be up to twice `max` long.
|
|
9
|
+
*/
|
|
10
|
+
export declare const ITEM_LINE_MAX = 1000;
|
|
11
|
+
export declare const LABEL_MAX = 80;
|
|
12
|
+
export declare function escapeQuoted(s: string, max: number): string;
|
|
13
|
+
/**
|
|
14
|
+
* Plan Task 30 (I-4 overruled): the CONTROL_TO_SPACE and C1_STRIP steps of escapeQuoted only, with no quote escaping and
|
|
15
|
+
* no cap. A migrated 7a note loses its invisible characters and is never refused for them.
|
|
16
|
+
*/
|
|
17
|
+
export declare function stripInvisible(s: string): string;
|
|
18
|
+
/** \u00A710.3 "At ingest": every label field loses its control characters before it is stored. */
|
|
19
|
+
export declare function stripLabelAtIngest(s: string): string;
|
|
20
|
+
export declare function pinnedLine(pin: string): string;
|
|
21
|
+
export declare function forgotLine(text: string): string;
|
|
22
|
+
/** OQ-P2: true for exactly the shapes pinnedLine and forgotLine produce (single line, since the escape removes line breaks). Such a line is never saved as an agent reply. */
|
|
23
|
+
export declare function isBridgeMemoryLine(text: string): boolean;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const MEMORY_INDEX_INFO = "securecoms/agent-memory-index/v1";
|
|
2
|
+
export declare const QUERY_MACS_MAX = 64;
|
|
3
|
+
export declare function deriveMemoryIndexKey(seed: Uint8Array): Buffer;
|
|
4
|
+
/** HMAC-SHA-256 truncated to 16 bytes. */
|
|
5
|
+
export declare function tokenMac(key: Uint8Array, token: string): Uint8Array;
|
|
6
|
+
export declare function itemTokenMacs(key: Uint8Array, text: string): Uint8Array[];
|
|
7
|
+
/** The query's tokens (stop words dropped) and their L-3 expansions, de-duplicated, capped at `max` MACs. */
|
|
8
|
+
export declare function queryTokenMacs(key: Uint8Array, text: string, max?: number): Uint8Array[];
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified memory (spec section 7.7; U9, U11, U16, L-1, R2-L-3): the ONLY caller of SqliteMlsStateStore.insertItems. Steps:
|
|
3
|
+
* the caller has captured U11 synchronously; here 4 (secret filter on the full text), 5 (chunk), 6 (seal, embed, MAC,
|
|
4
|
+
* outside the mutex), 7 (commit under the mutex: the trust decision, then one transaction), 8 (idempotent key; owner
|
|
5
|
+
* replaces reference, never the reverse). Logs are content-free (U17).
|
|
6
|
+
*/
|
|
7
|
+
import type { ItemCapture, ItemKind, ItemSource, ItemTrust, ScopeKind, SqliteMlsStateStore } from "@agentvault/connect-mls-store";
|
|
8
|
+
import { type ServiceLog } from "../../service-log.js";
|
|
9
|
+
import type { Embedder } from "../embedder/client.js";
|
|
10
|
+
import type { RankingClient } from "./ranking-client.js";
|
|
11
|
+
export type ItemSkip = "secret" | "empty" | "cleared" | "room_cleared" | "disabled" | "purged" | "duplicate" | "changed" | "pin_cap" | "quota" | "invalid" | "store" | "mode" | "message";
|
|
12
|
+
export interface ItemWriteRequest {
|
|
13
|
+
scopeKind: ScopeKind;
|
|
14
|
+
scopeId: string;
|
|
15
|
+
channelId: string;
|
|
16
|
+
kind: ItemKind;
|
|
17
|
+
originId: string;
|
|
18
|
+
originAt: number;
|
|
19
|
+
source: ItemSource;
|
|
20
|
+
trust: ItemTrust;
|
|
21
|
+
text: string;
|
|
22
|
+
expect: ItemCapture;
|
|
23
|
+
sourceRef?: string;
|
|
24
|
+
vec?: Float32Array | null;
|
|
25
|
+
vecModelId?: string | null;
|
|
26
|
+
/** Pins and migrated notes: one item, never chunked. */
|
|
27
|
+
noChunk?: true;
|
|
28
|
+
/** Task 30: a migrated source row changed since the marker: replace the item with the same source_ref, same trust. */
|
|
29
|
+
refresh?: true;
|
|
30
|
+
/** R-U19, migration only (Task 30). */
|
|
31
|
+
capExempt?: true;
|
|
32
|
+
/** Plan audit HIGH-2: embed nothing inline; every part goes pending to ItemReembed (Task 30's migration). */
|
|
33
|
+
pendingOnly?: true;
|
|
34
|
+
/** Task 28 (pins): the caller's last W1/W2 re-check, evaluated FIRST inside the memory mutex; a veto writes nothing. */
|
|
35
|
+
precommit?: () => "mode" | "message" | null;
|
|
36
|
+
}
|
|
37
|
+
export interface ItemBookDeps {
|
|
38
|
+
store: Pick<SqliteMlsStateStore, "reserveItemIds" | "sealItemRecord" | "sealItemVec" | "insertItems" | "itemByOrigin" | "openItemRecord" | "evictBatch">;
|
|
39
|
+
embedder: Embedder;
|
|
40
|
+
modelId: string;
|
|
41
|
+
indexKey: Uint8Array;
|
|
42
|
+
exclusive: <T>(fn: () => Promise<T>) => Promise<T>;
|
|
43
|
+
ranking: Pick<RankingClient, "added" | "removed">;
|
|
44
|
+
/** R-U11: false on win32 while WINDOWS_SEMANTIC_ENABLED is false: no embed job runs. */
|
|
45
|
+
embedOn: () => boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Plan audit HIGH-2: true while any call heartbeat of this OS user is live (the 7b E1.4 pause ItemReembed already
|
|
48
|
+
* respects). While true the write path embeds nothing inline; every part goes pending.
|
|
49
|
+
*/
|
|
50
|
+
paused: () => boolean;
|
|
51
|
+
now?: () => number;
|
|
52
|
+
log?: ServiceLog;
|
|
53
|
+
}
|
|
54
|
+
export declare class ItemBook {
|
|
55
|
+
private readonly d;
|
|
56
|
+
private evicting;
|
|
57
|
+
constructor(d: ItemBookDeps);
|
|
58
|
+
private skip;
|
|
59
|
+
commit(r: ItemWriteRequest): Promise<{
|
|
60
|
+
ok: true;
|
|
61
|
+
itemIds: number[];
|
|
62
|
+
} | {
|
|
63
|
+
ok: false;
|
|
64
|
+
reason: ItemSkip;
|
|
65
|
+
}>;
|
|
66
|
+
/** A post-commit hot-set notification: a throw is logged (content-free) and swallowed, never turned into a write failure. */
|
|
67
|
+
private rankingNote;
|
|
68
|
+
/** H-7: batches of at most EVICT_BATCH_MAX rows, a yield between batches, one loop at a time. */
|
|
69
|
+
evictLoop(): Promise<void>;
|
|
70
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Unified memory (spec section 7.7 step 6, 7b section 8): pending items are embedded in the background, 16 per tick, paused during calls, never on Windows (R-U11). */
|
|
2
|
+
import type { SqliteMlsStateStore } from "@agentvault/connect-mls-store";
|
|
3
|
+
import type { Embedder } from "../embedder/client.js";
|
|
4
|
+
import { type ServiceLog } from "../../service-log.js";
|
|
5
|
+
import type { RankingClient } from "./ranking-client.js";
|
|
6
|
+
export declare class ItemReembed {
|
|
7
|
+
private readonly d;
|
|
8
|
+
private running;
|
|
9
|
+
constructor(d: {
|
|
10
|
+
ranking: Pick<RankingClient, "pending" | "added">;
|
|
11
|
+
store: Pick<SqliteMlsStateStore, "itemCapture" | "sealItemVec" | "updateItemVec">;
|
|
12
|
+
embedder: Embedder;
|
|
13
|
+
exclusive: <T>(fn: () => Promise<T>) => Promise<T>;
|
|
14
|
+
modelId: string;
|
|
15
|
+
paused: () => boolean;
|
|
16
|
+
embedOn: () => boolean;
|
|
17
|
+
log?: ServiceLog;
|
|
18
|
+
});
|
|
19
|
+
tick(): Promise<number>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified memory (spec 2026-10-10 section 14.1, section 14.5; R-U12, M-1, L-1, H-5, H-B; plan audit HIGH-1, HIGH-2, M-4):
|
|
3
|
+
* migrate, per conversation, 7a notes (pins), 7b recall (owner auto items, vectors reused) and OWNER-DM history (reference,
|
|
4
|
+
* whatever its role) into agent_memory_item, in that order (L-1). Room and person-DM history and agent_memory_fact are never
|
|
5
|
+
* migrated. Idempotent on the item key. One marker per (source, conversation): a conversation never visited, or
|
|
6
|
+
* unclassifiable, holds back nobody else's (HIGH-1). Every write is pendingOnly: the migration never embeds (HIGH-2).
|
|
7
|
+
* At most MIGRATION_BATCH source rows per read, a yield between batches (OQ-P7). Never called on a turn's path.
|
|
8
|
+
*
|
|
9
|
+
* U11 (section 14.1 "a clear or purge during migration wins"): the capture is taken once per conversation, before its first
|
|
10
|
+
* read, and every item of that conversation is checked against it in the insert transaction. The first refusal for a
|
|
11
|
+
* clear, a room clear, a disable or a purge stops the conversation. A Clear all raises every conversation's history
|
|
12
|
+
* marker to just past the clear in the store's clear transaction (fix round 1), so no later run re-imports pre-clear
|
|
13
|
+
* history (a clear keeps history, section 14.4). After a disable or a purge the markers stay, so a later run resumes.
|
|
14
|
+
*/
|
|
15
|
+
import { type SqliteMlsStateStore } from "@agentvault/connect-mls-store";
|
|
16
|
+
import { type ServiceLog } from "../../service-log.js";
|
|
17
|
+
import type { ItemBook } from "./item-book.js";
|
|
18
|
+
/** Section 14.5: true while the 7a/7b tables are kept (one release). The release that drops them sets this false (the sweep stops). */
|
|
19
|
+
export declare const OLD_TABLES_PRESENT = true;
|
|
20
|
+
/** OQ-P7: at most this many source rows per read; a yield between reads. */
|
|
21
|
+
export declare const MIGRATION_BATCH = 100;
|
|
22
|
+
export type MigrationClass = {
|
|
23
|
+
kind: "owner";
|
|
24
|
+
ownerId: string;
|
|
25
|
+
} | {
|
|
26
|
+
kind: "excluded";
|
|
27
|
+
} | {
|
|
28
|
+
kind: "unknown";
|
|
29
|
+
};
|
|
30
|
+
export interface MigrationDeps {
|
|
31
|
+
store: Pick<SqliteMlsStateStore, "readNotesForMigration" | "readRecallForMigration" | "readHistoryForMigration" | "noteConversationIds" | "recallConversationIds" | "historyConversationIds" | "recallTombstoned" | "migrationMarker" | "setMigrationMarker" | "itemCapture">;
|
|
32
|
+
items: Pick<ItemBook, "commit">;
|
|
33
|
+
/**
|
|
34
|
+
* OQ-P1: "owner" with the owner identity for an owner DM; "excluded" for a conversation persisted as non-DM (M-4:
|
|
35
|
+
* never migrated, never retried, holds nothing); "unknown" when it cannot be classified now (retried on a later run).
|
|
36
|
+
*/
|
|
37
|
+
classify: (conversationId: string) => MigrationClass;
|
|
38
|
+
/** Called after a conversation's notes committed pins (the bridge empties its pin cache under the memory mutex, as Task 28 does). */
|
|
39
|
+
pinsChanged?: () => Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Final review bridge I-3: shutdown. Checked before each conversation, each batch and each slice yield; a run that sees
|
|
42
|
+
* it stops (an abort, retried by a later run: markers cover only the batches that completed) so close() never waits
|
|
43
|
+
* on a migration ahead of the MLS drain.
|
|
44
|
+
*/
|
|
45
|
+
stopping?: () => boolean;
|
|
46
|
+
log?: ServiceLog;
|
|
47
|
+
}
|
|
48
|
+
export interface MigrationResult {
|
|
49
|
+
notes: number;
|
|
50
|
+
recall: number;
|
|
51
|
+
history: number;
|
|
52
|
+
/** Conversations: unknown now (retried later), excluded (M-4), stopped by a U11 refusal. */
|
|
53
|
+
skipped: number;
|
|
54
|
+
excluded: number;
|
|
55
|
+
aborted: number;
|
|
56
|
+
unreadable: number;
|
|
57
|
+
}
|
|
58
|
+
export declare function runMigration(d: MigrationDeps, o?: {
|
|
59
|
+
only?: string;
|
|
60
|
+
}): Promise<MigrationResult>;
|
|
61
|
+
/** Section 14.5, before anything is served, in ONE transaction (controller ruling C22): the per-source sweep, then the clear-generation check. */
|
|
62
|
+
export declare function reupgradeReconcile(store: Pick<SqliteMlsStateStore, "reupgradeReconcileUnified">): {
|
|
63
|
+
swept: {
|
|
64
|
+
notes: number;
|
|
65
|
+
recall: number;
|
|
66
|
+
};
|
|
67
|
+
cleared: number;
|
|
68
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type AbsenceRecord } from "../recall/membership-purge.js";
|
|
2
|
+
import { type ServiceLog } from "../../service-log.js";
|
|
3
|
+
export interface OwnerFacts {
|
|
4
|
+
isDm: boolean | undefined;
|
|
5
|
+
pin: {
|
|
6
|
+
identityId: string;
|
|
7
|
+
} | "anomalous" | undefined;
|
|
8
|
+
member: {
|
|
9
|
+
is_agent: boolean;
|
|
10
|
+
is_agent_owner: boolean;
|
|
11
|
+
} | undefined;
|
|
12
|
+
rosterKnown: boolean;
|
|
13
|
+
}
|
|
14
|
+
/** A scope id when the channel is a DM whose single pinned human is the agent's owner; null when it is not; "unknown" without a fresh roster or a pin. */
|
|
15
|
+
export declare function ownerScopeFrom(channelId: string, f: OwnerFacts): string | null | "unknown";
|
|
16
|
+
export interface OwnerScopeSweepDeps {
|
|
17
|
+
/**
|
|
18
|
+
* The owner-DM scope ids holding items (NUL-safe, CARRY-NUL), and (final review store I-2, bridge half) the scope of each
|
|
19
|
+
* recorded DM owner, so an owner change is seen on a channel that holds no items too.
|
|
20
|
+
*/
|
|
21
|
+
scopes(): string[];
|
|
22
|
+
/**
|
|
23
|
+
* Final review store I-2 (bridge half; ruling FINAL store I-2): the conversations whose 7a/7b rows a migration may still
|
|
24
|
+
* read. Each DM among them has its current owner recorded (first sight wins; a different recorded owner is left for
|
|
25
|
+
* the scope loop to settle with the same two-refresh evidence rule, and its purge raises that channel's markers).
|
|
26
|
+
*/
|
|
27
|
+
oldTableChannels?(): string[];
|
|
28
|
+
recordOwner?(channelId: string, ownerIdentityId: string): void;
|
|
29
|
+
/** Roster facts only: no kill switch, provider or memory-mode input (OQ-P3). `isDm` is the persisted flag (C8). */
|
|
30
|
+
factsOf(channelId: string): OwnerFacts;
|
|
31
|
+
absenceOf(scopeId: string): AbsenceRecord | undefined;
|
|
32
|
+
markAbsent(scopeId: string, now: number): AbsenceRecord;
|
|
33
|
+
clearAbsence(scopeId: string): void;
|
|
34
|
+
purge(scopeId: string): Promise<void>;
|
|
35
|
+
now(): number;
|
|
36
|
+
/** Shutdown: the sweep stops before the next scope (like MembershipPurgeSweeper). */
|
|
37
|
+
stopping?(): boolean;
|
|
38
|
+
log?: ServiceLog;
|
|
39
|
+
}
|
|
40
|
+
export declare class OwnerScopeSweeper {
|
|
41
|
+
private readonly d;
|
|
42
|
+
private running;
|
|
43
|
+
constructor(d: OwnerScopeSweepDeps);
|
|
44
|
+
sweep(): Promise<{
|
|
45
|
+
purged: number;
|
|
46
|
+
frozen: number;
|
|
47
|
+
}>;
|
|
48
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** H-1: frozen; a pin token on this list needs no owner occurrence. Contents approved under OQ-P6. */
|
|
2
|
+
export declare const FRAMING_WORDS: ReadonlySet<string>;
|
|
3
|
+
/**
|
|
4
|
+
* C-1 with R2-L-1: NFC first; then keep letters (L), numbers (N), punctuation (P), space separators (Zs), the 14 symbols
|
|
5
|
+
* `$ % & + = < > ~ ^ | # @ * /`, and combining marks Mn and Mc except the variation selectors (including U+180B-180F). Everything else (Cf, Co,
|
|
6
|
+
* Cn, Cc, tag characters, bidi controls, zero-width characters, other symbols) is stripped.
|
|
7
|
+
*/
|
|
8
|
+
export declare function allowlistPin(raw: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* H-1: every pin token not on FRAMING_WORDS must occur, as an exact token, in the owner's current message or in an
|
|
11
|
+
* owner-trust TYPED item delivered with this turn (the caller passes only COVERAGE_SOURCES texts). No stems, no allowance.
|
|
12
|
+
*/
|
|
13
|
+
export declare function pinCoverage(pinStripped: string, ownerMessage: string, coverTexts: readonly string[]): {
|
|
14
|
+
ok: true;
|
|
15
|
+
} | {
|
|
16
|
+
ok: false;
|
|
17
|
+
uncovered: number;
|
|
18
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type ServiceLog } from "../../service-log.js";
|
|
2
|
+
import type { FromWorker, OpenedItem, RankQuery, ToWorker, WorkerInit } from "./worker-protocol.js";
|
|
3
|
+
export interface RankingTransport {
|
|
4
|
+
post(m: ToWorker): void;
|
|
5
|
+
onMessage(cb: (m: FromWorker) => void): void;
|
|
6
|
+
onExit(cb: () => void): void;
|
|
7
|
+
terminate(): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
export declare function workerTransport(init: WorkerInit, distDir: string): RankingTransport;
|
|
10
|
+
export declare class RankingClient {
|
|
11
|
+
private readonly d;
|
|
12
|
+
private t;
|
|
13
|
+
/** Final review bridge I-2(b): set by close(); no call after it spawns a worker or posts. */
|
|
14
|
+
private closed;
|
|
15
|
+
private seq;
|
|
16
|
+
private readonly waiting;
|
|
17
|
+
private misses;
|
|
18
|
+
private hot;
|
|
19
|
+
constructor(d: {
|
|
20
|
+
spawn: () => RankingTransport;
|
|
21
|
+
log?: ServiceLog;
|
|
22
|
+
});
|
|
23
|
+
private transport;
|
|
24
|
+
private ask;
|
|
25
|
+
query(q: RankQuery, deadlineAt: number): Promise<{
|
|
26
|
+
items: OpenedItem[];
|
|
27
|
+
openFailed: number;
|
|
28
|
+
} | "late" | "failed">;
|
|
29
|
+
pins(scopeId: string, maxChars: number, deadlineAt: number): Promise<{
|
|
30
|
+
pins: OpenedItem[];
|
|
31
|
+
more: number;
|
|
32
|
+
} | "late" | "failed">;
|
|
33
|
+
pending(limit: number, deadlineAt: number): Promise<Extract<FromWorker, {
|
|
34
|
+
type: "pending";
|
|
35
|
+
}>["rows"] | "late" | "failed">;
|
|
36
|
+
/**
|
|
37
|
+
* Final review bridge I-2: hot-set notifications reach a RUNNING worker only. With none running they are no-ops (a worker
|
|
38
|
+
* spawned later warms from the database, so nothing is lost): a purge, clear or write while memory is off never starts
|
|
39
|
+
* a resident worker, nothing respawns after close(), and a post that throws never escapes into a committed write's path.
|
|
40
|
+
*/
|
|
41
|
+
private notify;
|
|
42
|
+
added(itemIds: number[]): void;
|
|
43
|
+
removed(itemIds: number[]): void;
|
|
44
|
+
reset(): void;
|
|
45
|
+
hotBytes(): number;
|
|
46
|
+
deadlineMisses(): number;
|
|
47
|
+
close(): Promise<void>;
|
|
48
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified memory (spec section 8.1, section 8.5; plan Task 27): the read limits, split out of ranking-core.ts so the
|
|
3
|
+
* main-thread read path (read-book.ts) never imports the module that holds the SQL (T-33). No imports.
|
|
4
|
+
*/
|
|
5
|
+
export declare const TYPED_ITEMS_MAX = 10;
|
|
6
|
+
export declare const VOICE_ITEMS_MAX = 3;
|
|
7
|
+
export declare const READ_CHARS_MAX = 6000;
|
|
8
|
+
export declare const PINNED_CHARS_MAX = 4000;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified memory (spec section 8.1 steps 3 to 6, section 8.3, section 8.5; R-U16 rev2, R-U18, H-7): ranking over a READ-ONLY connection.
|
|
3
|
+
* Runs inside the ranking worker in production (Task 14) and in-process only in tests and the eval (Task 35).
|
|
4
|
+
* Every embedded item is hot (int8, one float scale per vector); there is no cold vector path. Keyword MACs boost items
|
|
5
|
+
* already in the meaning list and are the only candidate source for an item still pending its embed and for the
|
|
6
|
+
* keyword-only read (no query vector, or warm off: Windows, R-U11). U1-G (architect recall-gate memo, section 4).
|
|
7
|
+
*/
|
|
8
|
+
import type { DatabaseSync } from "node:sqlite";
|
|
9
|
+
import type { FromWorker, ToWorker, WorkerInit } from "./worker-protocol.js";
|
|
10
|
+
export declare const DIMS = 384;
|
|
11
|
+
export declare const OWNER_SCOPE_PRIOR = 0.05;
|
|
12
|
+
export declare const RECENCY_FLOOR = 0.85;
|
|
13
|
+
/** U1-G (C): 0.15 -> 0.05, a holding value until a replaced-fact eval exists (Part 2). The 0.85 floor is unchanged. */
|
|
14
|
+
export declare const RECENCY_SLOPE = 0.05;
|
|
15
|
+
export declare const RRF_K = 60;
|
|
16
|
+
export declare const HOT_TOKEN_MAX_POSTINGS = 5000;
|
|
17
|
+
export declare const LIST_MAX = 50;
|
|
18
|
+
/** U1-G (A): section 8.3's freshness is a tie-break: it orders only fused scores within this of each other (float
|
|
19
|
+
* rounding; distinct RRF sums at k = 60 over ranks below 50 differ by far more). */
|
|
20
|
+
export declare const FRESHNESS_TIE_EPSILON = 1e-12;
|
|
21
|
+
/** U1-G (D): hot set slots reserved above the warm count, and the growth factor (was doubling). */
|
|
22
|
+
export declare const HOT_SET_HEADROOM = 1024;
|
|
23
|
+
export declare const HOT_SET_GROWTH = 1.25;
|
|
24
|
+
export { TYPED_ITEMS_MAX, VOICE_ITEMS_MAX, READ_CHARS_MAX, PINNED_CHARS_MAX } from "./ranking-core-constants.js";
|
|
25
|
+
/** section 8.5: max(0.85, 1 - age_days/365 x slope), slope 0.05 (U1-G). `slope` is the plan Task 35 eval override only. */
|
|
26
|
+
export declare function recencyWeight(ageDays: number, slope?: number): number;
|
|
27
|
+
/** U1-G (D): the warm reserve, min(count, N) plus the headroom (never N for every agent). */
|
|
28
|
+
export declare function hotSetReserve(embeddedCount: number): number;
|
|
29
|
+
/** U1-G (D): the capacity a grow to hold `need` slots picks: x1.25, capped at N, at least `need`. Past N (the store's
|
|
30
|
+
* eviction lags an insert batch) it steps by the headroom, so a long tail over N never grows one slot at a time. */
|
|
31
|
+
export declare function hotSetGrowth(cap: number, need: number): number;
|
|
32
|
+
/** Packed int8 vectors with a per-vector scale; swap-remove keeps it dense. */
|
|
33
|
+
export declare class HotSet {
|
|
34
|
+
private cap;
|
|
35
|
+
private n;
|
|
36
|
+
private q;
|
|
37
|
+
private scale;
|
|
38
|
+
private ids;
|
|
39
|
+
private at;
|
|
40
|
+
private scopeOf;
|
|
41
|
+
private pin;
|
|
42
|
+
private readonly slotOf;
|
|
43
|
+
private readonly scopes;
|
|
44
|
+
private readonly scopeIdx;
|
|
45
|
+
get size(): number;
|
|
46
|
+
get capacity(): number;
|
|
47
|
+
vectorBytes(): number;
|
|
48
|
+
bytes(): number;
|
|
49
|
+
/** U1-G (D): sets the capacity to at least `slots` in one allocation (the warm reserve). */
|
|
50
|
+
reserve(slots: number): void;
|
|
51
|
+
private grow;
|
|
52
|
+
/** The old buffers are dropped (unreferenced) as soon as they are copied. */
|
|
53
|
+
private resize;
|
|
54
|
+
add(e: {
|
|
55
|
+
itemId: number;
|
|
56
|
+
vec: Float32Array;
|
|
57
|
+
scopeId: string;
|
|
58
|
+
originAt: number;
|
|
59
|
+
pinned: boolean;
|
|
60
|
+
}): void;
|
|
61
|
+
remove(itemId: number): void;
|
|
62
|
+
clear(): void;
|
|
63
|
+
/** section 8.5: sim (int8 dot x scale), meaning threshold, then (sim + prior) x recency; the top `k` by that score. */
|
|
64
|
+
topK(query: Float32Array, allowed: ReadonlySet<string>, o: {
|
|
65
|
+
k: number;
|
|
66
|
+
nowMs: number;
|
|
67
|
+
ownerScope: string | null;
|
|
68
|
+
threshold: number;
|
|
69
|
+
prior?: number;
|
|
70
|
+
recencySlope?: number;
|
|
71
|
+
}): Array<{
|
|
72
|
+
itemId: number;
|
|
73
|
+
sim: number;
|
|
74
|
+
score: number;
|
|
75
|
+
}>;
|
|
76
|
+
}
|
|
77
|
+
export declare class RankingCore {
|
|
78
|
+
private readonly init;
|
|
79
|
+
private readonly db;
|
|
80
|
+
private readonly hot;
|
|
81
|
+
private readonly stmts;
|
|
82
|
+
constructor(init: WorkerInit, db: DatabaseSync);
|
|
83
|
+
/** Prepared once per distinct statement text (the keyword statement varies only with the MAC and scope counts). */
|
|
84
|
+
private stmt;
|
|
85
|
+
hotCount(): number;
|
|
86
|
+
hotCapacity(): number;
|
|
87
|
+
/** The meaning list a query would fuse (section 8.5 scores, the init's eval overrides applied when set). */
|
|
88
|
+
meaningScores(vec: Float32Array, scopes: readonly string[], ownerScope: string | null, nowMs: number): Array<{
|
|
89
|
+
itemId: number;
|
|
90
|
+
sim: number;
|
|
91
|
+
score: number;
|
|
92
|
+
}>;
|
|
93
|
+
hotBytes(): number;
|
|
94
|
+
private open;
|
|
95
|
+
private fields;
|
|
96
|
+
private row;
|
|
97
|
+
private addRow;
|
|
98
|
+
/** section 8.5 warm: one row per read, a yield between rows; returns the hot count. A disabled warm (win32 keyword-only) loads nothing. */
|
|
99
|
+
warm(): Promise<number>;
|
|
100
|
+
/** Opens a record; null on any failure or a source_ref mismatch (section 11.3). */
|
|
101
|
+
private openRecord;
|
|
102
|
+
private opened;
|
|
103
|
+
/** H-7: one batched statement, LIMIT 5,001 per MAC inside the allowed scopes; a MAC that fills its limit is skipped. */
|
|
104
|
+
private keyword;
|
|
105
|
+
/** section 8.1 step 5: message ids of the newest 50 history turns of the conversation, plus the current message. */
|
|
106
|
+
private excluded;
|
|
107
|
+
private rank;
|
|
108
|
+
private pins;
|
|
109
|
+
private pending;
|
|
110
|
+
/**
|
|
111
|
+
* Final review bridge M-4: `handle` with a per-message catch. A request that throws (a prepare failure, a decode throw)
|
|
112
|
+
* is answered `failed` instead of becoming an unhandled rejection that kills the worker; a notification that throws is
|
|
113
|
+
* dropped (a later request still warms from the database). Content-free: the error is never sent or logged.
|
|
114
|
+
*/
|
|
115
|
+
handleSafe(m: ToWorker): Promise<FromWorker | null>;
|
|
116
|
+
handle(m: ToWorker): Promise<FromWorker | null>;
|
|
117
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|