@titan-design/queue-mirror 0.2.0 → 0.3.1
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 +6 -0
- package/dist/hitl.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +13 -2
- package/dist/index.js.map +1 -1
- package/dist/sqlite.d.ts +1 -1
- package/dist/{types-BgRpl8NH.d.ts → types-CYi2j1Jz.d.ts} +5 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -9,6 +9,12 @@ close edits the item with `m.replace`.
|
|
|
9
9
|
- `QueueSource` and `MirrorState` ports, with `MemoryQueueSource` and `MemoryMirrorState`.
|
|
10
10
|
- `@titan-design/queue-mirror/hitl`: `hitlQueueSource(store, options)` over a hitl `GateStore`.
|
|
11
11
|
|
|
12
|
+
`runMirror` calls `source.tail()` before `reconcile()`, so a `QueueSource.tail` must connect
|
|
13
|
+
eagerly: the connection must exist when `tail()` returns, not at the first `next()`. A lazy
|
|
14
|
+
`async function*` misses items opened during reconcile. A source that cannot replay a gap
|
|
15
|
+
yields `{type: "resync", cursor}`, and the mirror reconciles against `open()` and commits
|
|
16
|
+
the cursor. A `rejected` verdict edits the item `refused: <detail>` and leaves it open.
|
|
17
|
+
|
|
12
18
|
Tier 2 of the titan-platform DAG. Depends on `@titan-design/matrix-bus` and
|
|
13
19
|
`@titan-design/hitl`. The root entry has no `node:` import.
|
|
14
20
|
|
package/dist/hitl.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { GateRecord, GateStore } from '@titan-design/hitl';
|
|
2
|
-
import { V as VerdictInput, Q as QueueSource } from './types-
|
|
2
|
+
import { V as VerdictInput, Q as QueueSource } from './types-CYi2j1Jz.js';
|
|
3
3
|
import '@titan-design/matrix-bus';
|
|
4
4
|
|
|
5
5
|
type GateKind = "approval_request" | "question";
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as QueueItem, Q as QueueSource, V as VerdictInput, C as CloseOutcome, R as ResolveResult, S as SourceEvent, M as MirrorState, P as PostedItem, b as StateChange, c as MirrorLogger } from './types-
|
|
1
|
+
import { a as QueueItem, Q as QueueSource, V as VerdictInput, C as CloseOutcome, R as ResolveResult, S as SourceEvent, M as MirrorState, P as PostedItem, b as StateChange, c as MirrorLogger } from './types-CYi2j1Jz.js';
|
|
2
2
|
import { ItemInput, ItemKind, SyncBatch, AppserviceClient, TitanItem } from '@titan-design/matrix-bus';
|
|
3
3
|
|
|
4
4
|
/** A base64-like run is a secret only if it mixes digits and both cases and no path segment is a plain word. */
|
package/dist/index.js
CHANGED
|
@@ -221,6 +221,7 @@ var SILENT = { info: () => {
|
|
|
221
221
|
} };
|
|
222
222
|
var itemTxnId = (sourceId) => `qm-${encodeURIComponent(sourceId)}`;
|
|
223
223
|
var editTxnId = (sourceId) => `qm-edit-${encodeURIComponent(sourceId)}`;
|
|
224
|
+
var rejectTxnId = (sourceId, resolutionEventId) => `qm-reject-${encodeURIComponent(sourceId)}-${encodeURIComponent(resolutionEventId)}`;
|
|
224
225
|
function closedStatus(event) {
|
|
225
226
|
if (event.outcome === "resolved") return event.label ? `resolved at the terminal: ${event.label}` : "resolved at the terminal";
|
|
226
227
|
return event.outcome;
|
|
@@ -246,12 +247,16 @@ var QueueMirror = class {
|
|
|
246
247
|
const pending = await this.source.open();
|
|
247
248
|
const pendingIds = new Set(pending.map((item) => item.id));
|
|
248
249
|
for (const item of this.state.openItems()) {
|
|
249
|
-
if (!pendingIds.has(item.sourceId)) await this.close(item, "resolved at the terminal");
|
|
250
|
+
if (!pendingIds.has(item.sourceId) && !this.resolving.has(item.sourceId)) await this.close(item, "resolved at the terminal");
|
|
250
251
|
}
|
|
251
252
|
for (const item of pending) if (!this.state.bySourceId(item.id)) await this.post(item);
|
|
252
253
|
}
|
|
253
254
|
async applySourceEvent(event) {
|
|
254
255
|
const cursor = event.cursor;
|
|
256
|
+
if (event.type === "resync") {
|
|
257
|
+
await this.reconcile();
|
|
258
|
+
return this.state.commit({ sourceCursor: cursor });
|
|
259
|
+
}
|
|
255
260
|
if (event.type === "opened") {
|
|
256
261
|
if (this.state.bySourceId(event.item.id)) return this.state.commit({ sourceCursor: cursor });
|
|
257
262
|
return this.post(event.item, cursor);
|
|
@@ -323,9 +328,15 @@ var QueueMirror = class {
|
|
|
323
328
|
const result = await this.resolveAtSource(item.sourceId, { verdict, text, resolutionEventId: event.event_id });
|
|
324
329
|
if (result.ok) return this.close(item, `resolved: ${verdict}`, { applied });
|
|
325
330
|
if (result.reason === "closed") return this.close(item, "already resolved", { applied });
|
|
326
|
-
this.
|
|
331
|
+
await this.markRefused(item, event.event_id, result.detail);
|
|
327
332
|
this.log.warn("rejected", { sourceId: item.sourceId, verdict, detail: result.detail });
|
|
328
333
|
}
|
|
334
|
+
/** Edits the status but leaves the item open, so the owner can still answer it. */
|
|
335
|
+
async markRefused(item, resolutionEventId, detail = "rejected") {
|
|
336
|
+
const edit = encodeEdit(item.eventId, item.record, `refused: ${detail}`);
|
|
337
|
+
await this.bus.send(this.options.roomId, "m.room.message", edit, rejectTxnId(item.sourceId, resolutionEventId));
|
|
338
|
+
this.state.commit({ applied: [resolutionEventId] });
|
|
339
|
+
}
|
|
329
340
|
async resolveAtSource(sourceId, verdict) {
|
|
330
341
|
this.resolving.add(sourceId);
|
|
331
342
|
try {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/redact.ts","../src/render.ts","../src/memory-source.ts","../src/memory-state.ts","../src/mirror.ts","../src/edit.ts","../src/run.ts"],"sourcesContent":["const PLACEHOLDER = \"[REDACTED]\";\n\nconst AUTH_HEADER = /\\b(Bearer|Basic)\\s+[A-Za-z0-9._~+/-]+=*/gi;\n// The optional quote after the name lets a JSON key such as \"password\": \"x\" match.\nconst KEY_VALUE = /\\b([A-Za-z0-9_-]*(?:token|key|password))([\"']?\\s*[=:]\\s*)(\"[^\"]*\"|'[^']*'|[^\\s&\"',;]+)/gi;\n// Exactly 40 hex characters is a git commit SHA, which the owner exempted so git previews stay approvable.\nconst HEX_RUN = /\\b(?:[0-9a-fA-F]{32,39}|[0-9a-fA-F]{41,})\\b/g;\nconst B64_RUN = /(?<![A-Za-z0-9+/_-])[A-Za-z0-9+/_-]{32,}={0,2}(?![A-Za-z0-9+/_=-])/g;\n\nconst PLAIN_WORD = /^(?:[a-z]{3,}|[A-Z]{3,})$/;\n\n/** A base64-like run is a secret only if it mixes digits and both cases and no path segment is a plain word. */\nexport function looksSecret(run: string): boolean {\n if (!/\\d/.test(run) || !/[A-Z]/.test(run) || !/[a-z]/.test(run)) return false;\n return !run.split(\"/\").some((segment) => PLAIN_WORD.test(segment));\n}\n\n/** Section 10 decision 3: masks auth headers, key=value secrets and long hex or base64 runs. */\nexport function redactPreview(text: string): { text: string; redacted: boolean } {\n let hits = 0;\n const hit = (replacement: string) => {\n hits += 1;\n return replacement;\n };\n const out = text\n .replace(AUTH_HEADER, (_match, scheme: string) => hit(`${scheme} ${PLACEHOLDER}`))\n .replace(KEY_VALUE, (_match, name: string, separator: string) => hit(`${name}${separator}${PLACEHOLDER}`))\n .replace(HEX_RUN, () => hit(PLACEHOLDER))\n .replace(B64_RUN, (run) => (looksSecret(run) ? hit(PLACEHOLDER) : run));\n return { text: out, redacted: hits > 0 };\n}\n","import { MAX_CONTENT_BYTES, contentBytes, encodeItem, type ItemInput } from \"@titan-design/matrix-bus\";\nimport { redactPreview } from \"./redact.js\";\nimport type { QueueItem } from \"./types.js\";\n\nconst MAX_FIT_ROUNDS = 16;\n\nfunction fits(input: ItemInput): boolean {\n return contentBytes(encodeItem(input)) <= MAX_CONTENT_BYTES;\n}\n\n// Slices on code points, so an astral character is never split into a lone surrogate.\nfunction shrink(value: string | undefined, round: number): string | undefined {\n if (value === undefined) return undefined;\n const points = Array.from(value);\n return `${points.slice(0, Math.floor(points.length / 2 ** round)).join(\"\")}…`;\n}\n\n/** Halves `inputPreview` and `text` until the encoded content fits, marking the item truncated. */\nexport function fitItem(input: ItemInput): ItemInput {\n if (fits(input)) return input;\n for (let round = 1; round <= MAX_FIT_ROUNDS; round += 1) {\n const cut = { ...input, input_preview: shrink(input.input_preview, round), text: shrink(input.text, round), truncated: true };\n if (fits(cut)) return cut;\n }\n return { ...input, input_preview: undefined, text: undefined, truncated: true };\n}\n\nfunction redactFields(item: QueueItem): { inputPreview?: string; text?: string; redacted: boolean } {\n if (item.kind === \"approval_request\" && item.inputPreview !== undefined) {\n const { text, redacted } = redactPreview(item.inputPreview);\n return { inputPreview: text, text: item.text, redacted };\n }\n if (item.kind === \"endorse_request\" && item.text !== undefined) {\n const { text, redacted } = redactPreview(item.text);\n return { inputPreview: item.inputPreview, text, redacted };\n }\n return { inputPreview: item.inputPreview, text: item.text, redacted: false };\n}\n\n/** Redacts before fitting, so a cut never strands the tail of a secret the patterns no longer see whole. */\nexport function toItemInput(item: QueueItem): ItemInput {\n const { inputPreview, text, redacted } = redactFields(item);\n return fitItem({\n kind: item.kind,\n machine: item.machine,\n session: item.session,\n agent_id: item.agentId,\n msg_id: item.id,\n at: item.at,\n tool_name: item.toolName,\n input_preview: inputPreview,\n recipient: item.recipient,\n text,\n truncated: false,\n redacted,\n });\n}\n\n/** A /sync filter that returns only the queue room's timeline, with no presence, receipts or account data. */\nexport function syncFilter(roomId: string): Record<string, unknown> {\n const none = { types: [] as string[] };\n return {\n presence: none,\n account_data: none,\n room: { rooms: [roomId], timeline: { limit: 50 }, state: none, ephemeral: none, account_data: none },\n };\n}\n","import { ITEM_KINDS, type ItemKind } from \"@titan-design/matrix-bus\";\nimport type { CloseOutcome, QueueItem, QueueSource, ResolveResult, SourceEvent, VerdictInput } from \"./types.js\";\n\n/** An in-memory QueueSource for tests; cursors are the 1-based position in its event log. */\nexport class MemoryQueueSource implements QueueSource {\n readonly resolutions: Array<{ id: string } & VerdictInput> = [];\n private readonly pending = new Map<string, QueueItem>();\n private readonly log: SourceEvent[] = [];\n private readonly waiters = new Set<() => void>();\n\n constructor(readonly kinds: readonly ItemKind[] = ITEM_KINDS) {}\n\n add(item: QueueItem): void {\n this.pending.set(item.id, item);\n this.append({ type: \"opened\", item, cursor: this.nextCursor() });\n }\n\n close(id: string, outcome: CloseOutcome, label?: string): void {\n if (!this.pending.delete(id)) return;\n this.append({ type: \"closed\", id, outcome, label, cursor: this.nextCursor() });\n }\n\n async open(): Promise<QueueItem[]> {\n return [...this.pending.values()];\n }\n\n async resolve(id: string, verdict: VerdictInput): Promise<ResolveResult> {\n if (!this.pending.has(id)) return { ok: false, reason: \"closed\" };\n this.resolutions.push({ id, ...verdict });\n this.close(id, \"resolved\", verdict.verdict);\n return { ok: true };\n }\n\n /** \"Now\" is fixed when tail is called, not at the first next(), so nothing added in between is missed. */\n tail(cursor: string | undefined, signal: AbortSignal): AsyncIterable<SourceEvent> {\n return this.stream(cursor === undefined ? this.log.length : Number(cursor), signal);\n }\n\n private async *stream(start: number, signal: AbortSignal): AsyncGenerator<SourceEvent> {\n let next = start;\n while (!signal.aborted) {\n while (next < this.log.length) yield this.log[next++] as SourceEvent;\n await this.nextAppend(signal);\n }\n }\n\n private nextCursor(): string {\n return String(this.log.length + 1);\n }\n\n private append(event: SourceEvent): void {\n this.log.push(event);\n for (const wake of [...this.waiters]) wake();\n }\n\n private nextAppend(signal: AbortSignal): Promise<void> {\n return new Promise((resolve) => {\n if (signal.aborted) return resolve();\n const done = () => {\n this.waiters.delete(done);\n signal.removeEventListener(\"abort\", done);\n resolve();\n };\n this.waiters.add(done);\n signal.addEventListener(\"abort\", done);\n });\n }\n}\n","import type { MirrorState, PostedItem, StateChange } from \"./types.js\";\n\nexport interface MirrorSnapshot {\n sourceCursor?: string;\n syncToken?: string;\n posted: PostedItem[];\n applied: string[];\n}\n\n/** An in-memory MirrorState; `snapshot` and `restore` stand in for a restart in replay tests. */\nexport class MemoryMirrorState implements MirrorState {\n private cursor: string | undefined;\n private token: string | undefined;\n private readonly items = new Map<string, PostedItem>();\n private readonly eventIndex = new Map<string, string>();\n private readonly applied = new Set<string>();\n\n static restore(snapshot: MirrorSnapshot): MemoryMirrorState {\n const state = new MemoryMirrorState();\n state.commit({ ...snapshot, posted: snapshot.posted.map((item) => ({ ...item })) });\n return state;\n }\n\n sourceCursor(): string | undefined {\n return this.cursor;\n }\n\n syncToken(): string | undefined {\n return this.token;\n }\n\n bySourceId(id: string): PostedItem | undefined {\n return this.items.get(id);\n }\n\n byEventId(eventId: string): PostedItem | undefined {\n const sourceId = this.eventIndex.get(eventId);\n return sourceId === undefined ? undefined : this.items.get(sourceId);\n }\n\n openItems(): PostedItem[] {\n return [...this.items.values()].filter((item) => item.status === \"open\");\n }\n\n hasApplied(resolutionEventId: string): boolean {\n return this.applied.has(resolutionEventId);\n }\n\n /** Applies posted before closed, so one change may post and close the same item. */\n commit(change: StateChange): void {\n for (const item of change.posted ?? []) {\n this.items.set(item.sourceId, { ...item });\n this.eventIndex.set(item.eventId, item.sourceId);\n }\n for (const id of change.closed ?? []) {\n const item = this.items.get(id);\n if (item) item.status = \"closed\";\n }\n for (const id of change.applied ?? []) this.applied.add(id);\n if (change.sourceCursor !== undefined) this.cursor = change.sourceCursor;\n if (change.syncToken !== undefined) this.token = change.syncToken;\n }\n\n snapshot(): MirrorSnapshot {\n return {\n sourceCursor: this.cursor,\n syncToken: this.token,\n posted: [...this.items.values()].map((item) => ({ ...item })),\n applied: [...this.applied],\n };\n }\n}\n","import { ITEM_KEY, encodeItem, foldResolution, type AppserviceClient, type ItemKind, type MatrixEvent, type Resolution, type SyncBatch } from \"@titan-design/matrix-bus\";\nimport { encodeEdit } from \"./edit.js\";\nimport { toItemInput } from \"./render.js\";\nimport type { Backoff } from \"./supervise.js\";\nimport type { MirrorLogger, MirrorState, PostedItem, QueueItem, QueueSource, ResolveResult, SourceEvent, VerdictInput } from \"./types.js\";\n\n/** The slice of AppserviceClient the mirror drives; tests pass a fake hub. */\nexport type MirrorBus = Pick<AppserviceClient, \"send\" | \"syncLoop\" | \"userId\">;\n\nexport interface MirrorOptions {\n ownerUserId: string;\n roomId: string;\n signal: AbortSignal;\n now?: () => number;\n /** Default expiry for approval_request items lacking expiresAt. */\n approvalTtlMs?: number;\n sweepIntervalMs?: number;\n syncTimeoutMs?: number;\n backoff?: Backoff;\n sleep?: (ms: number, signal: AbortSignal) => Promise<void>;\n logger?: MirrorLogger;\n}\n\n/** Steps exposed so tests drive the mirror deterministically; runMirror only loops over them. */\nexport interface Mirror {\n reconcile(): Promise<void>;\n applySourceEvent(event: SourceEvent): Promise<void>;\n applySyncBatch(batch: SyncBatch): Promise<void>;\n sweepExpired(): Promise<void>;\n}\n\nexport const SILENT: MirrorLogger = { info: () => {}, warn: () => {} };\n\n// Deterministic, so a re-send after a crash between send and commit is deduped by the homeserver.\nconst itemTxnId = (sourceId: string) => `qm-${encodeURIComponent(sourceId)}`;\nconst editTxnId = (sourceId: string) => `qm-edit-${encodeURIComponent(sourceId)}`;\n\nfunction closedStatus(event: Extract<SourceEvent, { type: \"closed\" }>): string {\n if (event.outcome === \"resolved\") return event.label ? `resolved at the terminal: ${event.label}` : \"resolved at the terminal\";\n return event.outcome;\n}\n\nclass QueueMirror implements Mirror {\n private readonly log: MirrorLogger;\n private readonly now: () => number;\n /** Source ids with a phone verdict in flight; their echoed source close must not win the edit. */\n private readonly resolving = new Set<string>();\n\n constructor(\n private readonly source: QueueSource,\n private readonly bus: MirrorBus,\n private readonly state: MirrorState,\n private readonly options: MirrorOptions,\n ) {\n this.log = options.logger ?? SILENT;\n this.now = options.now ?? Date.now;\n }\n\n async reconcile(): Promise<void> {\n const pending = await this.source.open();\n const pendingIds = new Set(pending.map((item) => item.id));\n for (const item of this.state.openItems()) {\n if (!pendingIds.has(item.sourceId)) await this.close(item, \"resolved at the terminal\");\n }\n for (const item of pending) if (!this.state.bySourceId(item.id)) await this.post(item);\n }\n\n async applySourceEvent(event: SourceEvent): Promise<void> {\n const cursor = event.cursor;\n if (event.type === \"opened\") {\n if (this.state.bySourceId(event.item.id)) return this.state.commit({ sourceCursor: cursor });\n return this.post(event.item, cursor);\n }\n const item = this.state.bySourceId(event.id);\n if (item?.status !== \"open\" || this.resolving.has(event.id)) return this.state.commit({ sourceCursor: cursor });\n await this.close(item, closedStatus(event), { sourceCursor: cursor });\n }\n\n /** Persists `since` only after every event is handled; a throw leaves it for the retry. */\n async applySyncBatch(batch: SyncBatch): Promise<void> {\n for (const event of batch.events) await this.applyEvent(event);\n this.state.commit({ syncToken: batch.since });\n }\n\n async sweepExpired(): Promise<void> {\n const now = this.now();\n for (const item of this.state.openItems()) {\n if (item.expiresAt !== undefined && item.expiresAt <= now) await this.close(item, \"expired\");\n }\n }\n\n private expiryOf(item: QueueItem): number | undefined {\n if (item.expiresAt !== undefined) return item.expiresAt;\n const ttl = this.options.approvalTtlMs;\n return item.kind === \"approval_request\" && ttl !== undefined ? item.at + ttl : undefined;\n }\n\n private async post(item: QueueItem, sourceCursor?: string): Promise<void> {\n const input = toItemInput(item);\n const content = encodeItem(input);\n const { event_id: eventId } = await this.bus.send(this.options.roomId, \"m.room.message\", content, itemTxnId(item.id));\n const posted: PostedItem = {\n sourceId: item.id,\n eventId,\n kind: item.kind,\n approvable: !input.redacted && !input.truncated,\n status: \"open\",\n expiresAt: this.expiryOf(item),\n record: content[ITEM_KEY],\n };\n this.state.commit({ posted: [posted], sourceCursor });\n this.log.info(\"posted\", { sourceId: item.id, eventId });\n }\n\n /** Edits first, then commits, so a crash in between replays into the same deduped edit. */\n private async close(item: PostedItem, status: string, change: { sourceCursor?: string; applied?: string[] } = {}): Promise<void> {\n const edit = encodeEdit(item.eventId, item.record, status);\n await this.bus.send(this.options.roomId, \"m.room.message\", edit, editTxnId(item.sourceId));\n this.state.commit({ ...change, closed: [item.sourceId] });\n this.log.info(\"closed\", { sourceId: item.sourceId, status });\n }\n\n private async applyEvent(event: MatrixEvent): Promise<void> {\n if (event.sender === this.bus.userId || this.state.hasApplied(event.event_id)) return;\n const open = this.state.openItems();\n const resolution = this.fold(event, open.filter((item) => item.approvable));\n if (resolution) return this.applyResolution(event, resolution);\n const refused = this.fold(event, open.filter((item) => !item.approvable));\n if (refused) return this.logRefusal(event, refused);\n if (isResolutionShaped(event)) this.log.info(\"ignored\", { eventId: event.event_id, sender: event.sender });\n }\n\n private fold(event: MatrixEvent, items: PostedItem[]): Resolution | null {\n const itemEventIds = new Map<string, ItemKind>(items.map((item) => [item.eventId, item.kind]));\n return foldResolution(event, { ownerUserId: this.options.ownerUserId, itemEventIds });\n }\n\n private logRefusal(event: MatrixEvent, refused: Resolution): void {\n const item = this.state.byEventId(refused.itemEventId);\n const reason = item?.record.truncated ? \"truncated\" : \"redacted\";\n this.log.warn(`refused: ${reason}`, { eventId: event.event_id, sourceId: item?.sourceId });\n }\n\n private async applyResolution(event: MatrixEvent, { itemEventId, verdict, text }: Resolution): Promise<void> {\n const item = this.state.byEventId(itemEventId) as PostedItem;\n const applied = [event.event_id];\n const result = await this.resolveAtSource(item.sourceId, { verdict, text, resolutionEventId: event.event_id });\n if (result.ok) return this.close(item, `resolved: ${verdict}`, { applied });\n if (result.reason === \"closed\") return this.close(item, \"already resolved\", { applied });\n this.state.commit({ applied });\n this.log.warn(\"rejected\", { sourceId: item.sourceId, verdict, detail: result.detail });\n }\n\n private async resolveAtSource(sourceId: string, verdict: VerdictInput): Promise<ResolveResult> {\n this.resolving.add(sourceId);\n try {\n return await this.source.resolve(sourceId, verdict);\n } finally {\n this.resolving.delete(sourceId);\n }\n }\n}\n\nconst RESOLUTION_TYPES = new Set([\"m.reaction\", \"m.room.message\", \"io.titan.resolution\"]);\n\nfunction isResolutionShaped(event: MatrixEvent): boolean {\n return RESOLUTION_TYPES.has(event.type) && event.state_key === undefined;\n}\n\nexport function createMirror(source: QueueSource, bus: MirrorBus, state: MirrorState, options: MirrorOptions): Mirror {\n return new QueueMirror(source, bus, state, options);\n}\n","import { ITEM_KEY, renderItemBody, type TitanItem } from \"@titan-design/matrix-bus\";\n\nfunction headline(record: TitanItem): string {\n return renderItemBody({ ...record, text: undefined }).split(\"\\n\")[0] ?? \"\";\n}\n\n/** An m.replace edit of an item: a short headline and status body, with the original record in m.new_content. */\nexport function encodeEdit(targetEventId: string, record: TitanItem, status: string): Record<string, unknown> {\n const body = `${headline(record)}\\n${status}`;\n return {\n msgtype: \"m.text\",\n body: `* ${body}`,\n \"m.new_content\": { msgtype: \"m.text\", body, [ITEM_KEY]: record },\n \"m.relates_to\": { rel_type: \"m.replace\", event_id: targetEventId },\n };\n}\n","import { createMirror, SILENT, type Mirror, type MirrorBus, type MirrorOptions } from \"./mirror.js\";\nimport { syncFilter } from \"./render.js\";\nimport { DEFAULT_BACKOFF, abortableSleep, superviseLoop } from \"./supervise.js\";\nimport type { MirrorState, QueueSource } from \"./types.js\";\n\ntype Body = (reset: () => void) => Promise<void>;\n\nfunction streamEnded(name: string, signal: AbortSignal): void {\n if (!signal.aborted) throw new Error(`${name} stream ended`);\n}\n\n// Opens the tail before reconciling, so an item opened during reconcile arrives through the tail.\nfunction sourceLoop(mirror: Mirror, source: QueueSource, state: MirrorState, signal: AbortSignal): Body {\n return async (reset) => {\n const events = source.tail(state.sourceCursor(), signal);\n await mirror.reconcile();\n for await (const event of events) {\n await mirror.applySourceEvent(event);\n reset();\n }\n streamEnded(\"source\", signal);\n };\n}\n\nfunction syncLoop(mirror: Mirror, bus: MirrorBus, state: MirrorState, options: MirrorOptions): Body {\n const { roomId, signal, syncTimeoutMs = 30_000 } = options;\n return async (reset) => {\n for await (const batch of bus.syncLoop({ since: state.syncToken(), filter: syncFilter(roomId), timeoutMs: syncTimeoutMs, signal })) {\n await mirror.applySyncBatch(batch);\n reset();\n }\n streamEnded(\"sync\", signal);\n };\n}\n\nfunction sweepLoop(mirror: Mirror, options: MirrorOptions, sleep: NonNullable<MirrorOptions[\"sleep\"]>): Body {\n const { signal, sweepIntervalMs = 5_000 } = options;\n return async (reset) => {\n while (!signal.aborted) {\n await mirror.sweepExpired();\n reset();\n await sleep(sweepIntervalMs, signal);\n }\n };\n}\n\n/** Runs the source tail (reconciling on each start), the sync loop and the expiry sweep until `signal` aborts. */\nexport async function runMirror(source: QueueSource, bus: MirrorBus, state: MirrorState, options: MirrorOptions): Promise<void> {\n const mirror = createMirror(source, bus, state, options);\n const sleep = options.sleep ?? abortableSleep;\n const supervise = { signal: options.signal, backoff: options.backoff ?? DEFAULT_BACKOFF, sleep, logger: options.logger ?? SILENT };\n await Promise.all([\n superviseLoop(\"source\", sourceLoop(mirror, source, state, options.signal), supervise),\n superviseLoop(\"sync\", syncLoop(mirror, bus, state, options), supervise),\n superviseLoop(\"sweep\", sweepLoop(mirror, options, sleep), supervise),\n ]);\n}\n"],"mappings":"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","names":["ITEM_KEY","encodeItem","encodeItem","ITEM_KEY"]}
|
|
1
|
+
{"version":3,"sources":["../src/redact.ts","../src/render.ts","../src/memory-source.ts","../src/memory-state.ts","../src/mirror.ts","../src/edit.ts","../src/run.ts"],"sourcesContent":["const PLACEHOLDER = \"[REDACTED]\";\n\nconst AUTH_HEADER = /\\b(Bearer|Basic)\\s+[A-Za-z0-9._~+/-]+=*/gi;\n// The optional quote after the name lets a JSON key such as \"password\": \"x\" match.\nconst KEY_VALUE = /\\b([A-Za-z0-9_-]*(?:token|key|password))([\"']?\\s*[=:]\\s*)(\"[^\"]*\"|'[^']*'|[^\\s&\"',;]+)/gi;\n// Exactly 40 hex characters is a git commit SHA, which the owner exempted so git previews stay approvable.\nconst HEX_RUN = /\\b(?:[0-9a-fA-F]{32,39}|[0-9a-fA-F]{41,})\\b/g;\nconst B64_RUN = /(?<![A-Za-z0-9+/_-])[A-Za-z0-9+/_-]{32,}={0,2}(?![A-Za-z0-9+/_=-])/g;\n\nconst PLAIN_WORD = /^(?:[a-z]{3,}|[A-Z]{3,})$/;\n\n/** A base64-like run is a secret only if it mixes digits and both cases and no path segment is a plain word. */\nexport function looksSecret(run: string): boolean {\n if (!/\\d/.test(run) || !/[A-Z]/.test(run) || !/[a-z]/.test(run)) return false;\n return !run.split(\"/\").some((segment) => PLAIN_WORD.test(segment));\n}\n\n/** Section 10 decision 3: masks auth headers, key=value secrets and long hex or base64 runs. */\nexport function redactPreview(text: string): { text: string; redacted: boolean } {\n let hits = 0;\n const hit = (replacement: string) => {\n hits += 1;\n return replacement;\n };\n const out = text\n .replace(AUTH_HEADER, (_match, scheme: string) => hit(`${scheme} ${PLACEHOLDER}`))\n .replace(KEY_VALUE, (_match, name: string, separator: string) => hit(`${name}${separator}${PLACEHOLDER}`))\n .replace(HEX_RUN, () => hit(PLACEHOLDER))\n .replace(B64_RUN, (run) => (looksSecret(run) ? hit(PLACEHOLDER) : run));\n return { text: out, redacted: hits > 0 };\n}\n","import { MAX_CONTENT_BYTES, contentBytes, encodeItem, type ItemInput } from \"@titan-design/matrix-bus\";\nimport { redactPreview } from \"./redact.js\";\nimport type { QueueItem } from \"./types.js\";\n\nconst MAX_FIT_ROUNDS = 16;\n\nfunction fits(input: ItemInput): boolean {\n return contentBytes(encodeItem(input)) <= MAX_CONTENT_BYTES;\n}\n\n// Slices on code points, so an astral character is never split into a lone surrogate.\nfunction shrink(value: string | undefined, round: number): string | undefined {\n if (value === undefined) return undefined;\n const points = Array.from(value);\n return `${points.slice(0, Math.floor(points.length / 2 ** round)).join(\"\")}…`;\n}\n\n/** Halves `inputPreview` and `text` until the encoded content fits, marking the item truncated. */\nexport function fitItem(input: ItemInput): ItemInput {\n if (fits(input)) return input;\n for (let round = 1; round <= MAX_FIT_ROUNDS; round += 1) {\n const cut = { ...input, input_preview: shrink(input.input_preview, round), text: shrink(input.text, round), truncated: true };\n if (fits(cut)) return cut;\n }\n return { ...input, input_preview: undefined, text: undefined, truncated: true };\n}\n\nfunction redactFields(item: QueueItem): { inputPreview?: string; text?: string; redacted: boolean } {\n if (item.kind === \"approval_request\" && item.inputPreview !== undefined) {\n const { text, redacted } = redactPreview(item.inputPreview);\n return { inputPreview: text, text: item.text, redacted };\n }\n if (item.kind === \"endorse_request\" && item.text !== undefined) {\n const { text, redacted } = redactPreview(item.text);\n return { inputPreview: item.inputPreview, text, redacted };\n }\n return { inputPreview: item.inputPreview, text: item.text, redacted: false };\n}\n\n/** Redacts before fitting, so a cut never strands the tail of a secret the patterns no longer see whole. */\nexport function toItemInput(item: QueueItem): ItemInput {\n const { inputPreview, text, redacted } = redactFields(item);\n return fitItem({\n kind: item.kind,\n machine: item.machine,\n session: item.session,\n agent_id: item.agentId,\n msg_id: item.id,\n at: item.at,\n tool_name: item.toolName,\n input_preview: inputPreview,\n recipient: item.recipient,\n text,\n truncated: false,\n redacted,\n });\n}\n\n/** A /sync filter that returns only the queue room's timeline, with no presence, receipts or account data. */\nexport function syncFilter(roomId: string): Record<string, unknown> {\n const none = { types: [] as string[] };\n return {\n presence: none,\n account_data: none,\n room: { rooms: [roomId], timeline: { limit: 50 }, state: none, ephemeral: none, account_data: none },\n };\n}\n","import { ITEM_KINDS, type ItemKind } from \"@titan-design/matrix-bus\";\nimport type { CloseOutcome, QueueItem, QueueSource, ResolveResult, SourceEvent, VerdictInput } from \"./types.js\";\n\n/** An in-memory QueueSource for tests; cursors are the 1-based position in its event log. */\nexport class MemoryQueueSource implements QueueSource {\n readonly resolutions: Array<{ id: string } & VerdictInput> = [];\n private readonly pending = new Map<string, QueueItem>();\n private readonly log: SourceEvent[] = [];\n private readonly waiters = new Set<() => void>();\n\n constructor(readonly kinds: readonly ItemKind[] = ITEM_KINDS) {}\n\n add(item: QueueItem): void {\n this.pending.set(item.id, item);\n this.append({ type: \"opened\", item, cursor: this.nextCursor() });\n }\n\n close(id: string, outcome: CloseOutcome, label?: string): void {\n if (!this.pending.delete(id)) return;\n this.append({ type: \"closed\", id, outcome, label, cursor: this.nextCursor() });\n }\n\n async open(): Promise<QueueItem[]> {\n return [...this.pending.values()];\n }\n\n async resolve(id: string, verdict: VerdictInput): Promise<ResolveResult> {\n if (!this.pending.has(id)) return { ok: false, reason: \"closed\" };\n this.resolutions.push({ id, ...verdict });\n this.close(id, \"resolved\", verdict.verdict);\n return { ok: true };\n }\n\n /** \"Now\" is fixed when tail is called, not at the first next(), so nothing added in between is missed. */\n tail(cursor: string | undefined, signal: AbortSignal): AsyncIterable<SourceEvent> {\n return this.stream(cursor === undefined ? this.log.length : Number(cursor), signal);\n }\n\n private async *stream(start: number, signal: AbortSignal): AsyncGenerator<SourceEvent> {\n let next = start;\n while (!signal.aborted) {\n while (next < this.log.length) yield this.log[next++] as SourceEvent;\n await this.nextAppend(signal);\n }\n }\n\n private nextCursor(): string {\n return String(this.log.length + 1);\n }\n\n private append(event: SourceEvent): void {\n this.log.push(event);\n for (const wake of [...this.waiters]) wake();\n }\n\n private nextAppend(signal: AbortSignal): Promise<void> {\n return new Promise((resolve) => {\n if (signal.aborted) return resolve();\n const done = () => {\n this.waiters.delete(done);\n signal.removeEventListener(\"abort\", done);\n resolve();\n };\n this.waiters.add(done);\n signal.addEventListener(\"abort\", done);\n });\n }\n}\n","import type { MirrorState, PostedItem, StateChange } from \"./types.js\";\n\nexport interface MirrorSnapshot {\n sourceCursor?: string;\n syncToken?: string;\n posted: PostedItem[];\n applied: string[];\n}\n\n/** An in-memory MirrorState; `snapshot` and `restore` stand in for a restart in replay tests. */\nexport class MemoryMirrorState implements MirrorState {\n private cursor: string | undefined;\n private token: string | undefined;\n private readonly items = new Map<string, PostedItem>();\n private readonly eventIndex = new Map<string, string>();\n private readonly applied = new Set<string>();\n\n static restore(snapshot: MirrorSnapshot): MemoryMirrorState {\n const state = new MemoryMirrorState();\n state.commit({ ...snapshot, posted: snapshot.posted.map((item) => ({ ...item })) });\n return state;\n }\n\n sourceCursor(): string | undefined {\n return this.cursor;\n }\n\n syncToken(): string | undefined {\n return this.token;\n }\n\n bySourceId(id: string): PostedItem | undefined {\n return this.items.get(id);\n }\n\n byEventId(eventId: string): PostedItem | undefined {\n const sourceId = this.eventIndex.get(eventId);\n return sourceId === undefined ? undefined : this.items.get(sourceId);\n }\n\n openItems(): PostedItem[] {\n return [...this.items.values()].filter((item) => item.status === \"open\");\n }\n\n hasApplied(resolutionEventId: string): boolean {\n return this.applied.has(resolutionEventId);\n }\n\n /** Applies posted before closed, so one change may post and close the same item. */\n commit(change: StateChange): void {\n for (const item of change.posted ?? []) {\n this.items.set(item.sourceId, { ...item });\n this.eventIndex.set(item.eventId, item.sourceId);\n }\n for (const id of change.closed ?? []) {\n const item = this.items.get(id);\n if (item) item.status = \"closed\";\n }\n for (const id of change.applied ?? []) this.applied.add(id);\n if (change.sourceCursor !== undefined) this.cursor = change.sourceCursor;\n if (change.syncToken !== undefined) this.token = change.syncToken;\n }\n\n snapshot(): MirrorSnapshot {\n return {\n sourceCursor: this.cursor,\n syncToken: this.token,\n posted: [...this.items.values()].map((item) => ({ ...item })),\n applied: [...this.applied],\n };\n }\n}\n","import { ITEM_KEY, encodeItem, foldResolution, type AppserviceClient, type ItemKind, type MatrixEvent, type Resolution, type SyncBatch } from \"@titan-design/matrix-bus\";\nimport { encodeEdit } from \"./edit.js\";\nimport { toItemInput } from \"./render.js\";\nimport type { Backoff } from \"./supervise.js\";\nimport type { MirrorLogger, MirrorState, PostedItem, QueueItem, QueueSource, ResolveResult, SourceEvent, VerdictInput } from \"./types.js\";\n\n/** The slice of AppserviceClient the mirror drives; tests pass a fake hub. */\nexport type MirrorBus = Pick<AppserviceClient, \"send\" | \"syncLoop\" | \"userId\">;\n\nexport interface MirrorOptions {\n ownerUserId: string;\n roomId: string;\n signal: AbortSignal;\n now?: () => number;\n /** Default expiry for approval_request items lacking expiresAt. */\n approvalTtlMs?: number;\n sweepIntervalMs?: number;\n syncTimeoutMs?: number;\n backoff?: Backoff;\n sleep?: (ms: number, signal: AbortSignal) => Promise<void>;\n logger?: MirrorLogger;\n}\n\n/** Steps exposed so tests drive the mirror deterministically; runMirror only loops over them. */\nexport interface Mirror {\n reconcile(): Promise<void>;\n applySourceEvent(event: SourceEvent): Promise<void>;\n applySyncBatch(batch: SyncBatch): Promise<void>;\n sweepExpired(): Promise<void>;\n}\n\nexport const SILENT: MirrorLogger = { info: () => {}, warn: () => {} };\n\n// Deterministic, so a re-send after a crash between send and commit is deduped by the homeserver.\nconst itemTxnId = (sourceId: string) => `qm-${encodeURIComponent(sourceId)}`;\nconst editTxnId = (sourceId: string) => `qm-edit-${encodeURIComponent(sourceId)}`;\n// Distinct from editTxnId, which the final close edit must still be free to use.\nconst rejectTxnId = (sourceId: string, resolutionEventId: string) =>\n `qm-reject-${encodeURIComponent(sourceId)}-${encodeURIComponent(resolutionEventId)}`;\n\nfunction closedStatus(event: Extract<SourceEvent, { type: \"closed\" }>): string {\n if (event.outcome === \"resolved\") return event.label ? `resolved at the terminal: ${event.label}` : \"resolved at the terminal\";\n return event.outcome;\n}\n\nclass QueueMirror implements Mirror {\n private readonly log: MirrorLogger;\n private readonly now: () => number;\n /** Source ids with a phone verdict in flight; their echoed source close must not win the edit. */\n private readonly resolving = new Set<string>();\n\n constructor(\n private readonly source: QueueSource,\n private readonly bus: MirrorBus,\n private readonly state: MirrorState,\n private readonly options: MirrorOptions,\n ) {\n this.log = options.logger ?? SILENT;\n this.now = options.now ?? Date.now;\n }\n\n async reconcile(): Promise<void> {\n const pending = await this.source.open();\n const pendingIds = new Set(pending.map((item) => item.id));\n for (const item of this.state.openItems()) {\n if (!pendingIds.has(item.sourceId) && !this.resolving.has(item.sourceId)) await this.close(item, \"resolved at the terminal\");\n }\n for (const item of pending) if (!this.state.bySourceId(item.id)) await this.post(item);\n }\n\n async applySourceEvent(event: SourceEvent): Promise<void> {\n const cursor = event.cursor;\n if (event.type === \"resync\") {\n await this.reconcile();\n return this.state.commit({ sourceCursor: cursor });\n }\n if (event.type === \"opened\") {\n if (this.state.bySourceId(event.item.id)) return this.state.commit({ sourceCursor: cursor });\n return this.post(event.item, cursor);\n }\n const item = this.state.bySourceId(event.id);\n if (item?.status !== \"open\" || this.resolving.has(event.id)) return this.state.commit({ sourceCursor: cursor });\n await this.close(item, closedStatus(event), { sourceCursor: cursor });\n }\n\n /** Persists `since` only after every event is handled; a throw leaves it for the retry. */\n async applySyncBatch(batch: SyncBatch): Promise<void> {\n for (const event of batch.events) await this.applyEvent(event);\n this.state.commit({ syncToken: batch.since });\n }\n\n async sweepExpired(): Promise<void> {\n const now = this.now();\n for (const item of this.state.openItems()) {\n if (item.expiresAt !== undefined && item.expiresAt <= now) await this.close(item, \"expired\");\n }\n }\n\n private expiryOf(item: QueueItem): number | undefined {\n if (item.expiresAt !== undefined) return item.expiresAt;\n const ttl = this.options.approvalTtlMs;\n return item.kind === \"approval_request\" && ttl !== undefined ? item.at + ttl : undefined;\n }\n\n private async post(item: QueueItem, sourceCursor?: string): Promise<void> {\n const input = toItemInput(item);\n const content = encodeItem(input);\n const { event_id: eventId } = await this.bus.send(this.options.roomId, \"m.room.message\", content, itemTxnId(item.id));\n const posted: PostedItem = {\n sourceId: item.id,\n eventId,\n kind: item.kind,\n approvable: !input.redacted && !input.truncated,\n status: \"open\",\n expiresAt: this.expiryOf(item),\n record: content[ITEM_KEY],\n };\n this.state.commit({ posted: [posted], sourceCursor });\n this.log.info(\"posted\", { sourceId: item.id, eventId });\n }\n\n /** Edits first, then commits, so a crash in between replays into the same deduped edit. */\n private async close(item: PostedItem, status: string, change: { sourceCursor?: string; applied?: string[] } = {}): Promise<void> {\n const edit = encodeEdit(item.eventId, item.record, status);\n await this.bus.send(this.options.roomId, \"m.room.message\", edit, editTxnId(item.sourceId));\n this.state.commit({ ...change, closed: [item.sourceId] });\n this.log.info(\"closed\", { sourceId: item.sourceId, status });\n }\n\n private async applyEvent(event: MatrixEvent): Promise<void> {\n if (event.sender === this.bus.userId || this.state.hasApplied(event.event_id)) return;\n const open = this.state.openItems();\n const resolution = this.fold(event, open.filter((item) => item.approvable));\n if (resolution) return this.applyResolution(event, resolution);\n const refused = this.fold(event, open.filter((item) => !item.approvable));\n if (refused) return this.logRefusal(event, refused);\n if (isResolutionShaped(event)) this.log.info(\"ignored\", { eventId: event.event_id, sender: event.sender });\n }\n\n private fold(event: MatrixEvent, items: PostedItem[]): Resolution | null {\n const itemEventIds = new Map<string, ItemKind>(items.map((item) => [item.eventId, item.kind]));\n return foldResolution(event, { ownerUserId: this.options.ownerUserId, itemEventIds });\n }\n\n private logRefusal(event: MatrixEvent, refused: Resolution): void {\n const item = this.state.byEventId(refused.itemEventId);\n const reason = item?.record.truncated ? \"truncated\" : \"redacted\";\n this.log.warn(`refused: ${reason}`, { eventId: event.event_id, sourceId: item?.sourceId });\n }\n\n private async applyResolution(event: MatrixEvent, { itemEventId, verdict, text }: Resolution): Promise<void> {\n const item = this.state.byEventId(itemEventId) as PostedItem;\n const applied = [event.event_id];\n const result = await this.resolveAtSource(item.sourceId, { verdict, text, resolutionEventId: event.event_id });\n if (result.ok) return this.close(item, `resolved: ${verdict}`, { applied });\n if (result.reason === \"closed\") return this.close(item, \"already resolved\", { applied });\n await this.markRefused(item, event.event_id, result.detail);\n this.log.warn(\"rejected\", { sourceId: item.sourceId, verdict, detail: result.detail });\n }\n\n /** Edits the status but leaves the item open, so the owner can still answer it. */\n private async markRefused(item: PostedItem, resolutionEventId: string, detail = \"rejected\"): Promise<void> {\n const edit = encodeEdit(item.eventId, item.record, `refused: ${detail}`);\n await this.bus.send(this.options.roomId, \"m.room.message\", edit, rejectTxnId(item.sourceId, resolutionEventId));\n this.state.commit({ applied: [resolutionEventId] });\n }\n\n private async resolveAtSource(sourceId: string, verdict: VerdictInput): Promise<ResolveResult> {\n this.resolving.add(sourceId);\n try {\n return await this.source.resolve(sourceId, verdict);\n } finally {\n this.resolving.delete(sourceId);\n }\n }\n}\n\nconst RESOLUTION_TYPES = new Set([\"m.reaction\", \"m.room.message\", \"io.titan.resolution\"]);\n\nfunction isResolutionShaped(event: MatrixEvent): boolean {\n return RESOLUTION_TYPES.has(event.type) && event.state_key === undefined;\n}\n\nexport function createMirror(source: QueueSource, bus: MirrorBus, state: MirrorState, options: MirrorOptions): Mirror {\n return new QueueMirror(source, bus, state, options);\n}\n","import { ITEM_KEY, renderItemBody, type TitanItem } from \"@titan-design/matrix-bus\";\n\nfunction headline(record: TitanItem): string {\n return renderItemBody({ ...record, text: undefined }).split(\"\\n\")[0] ?? \"\";\n}\n\n/** An m.replace edit of an item: a short headline and status body, with the original record in m.new_content. */\nexport function encodeEdit(targetEventId: string, record: TitanItem, status: string): Record<string, unknown> {\n const body = `${headline(record)}\\n${status}`;\n return {\n msgtype: \"m.text\",\n body: `* ${body}`,\n \"m.new_content\": { msgtype: \"m.text\", body, [ITEM_KEY]: record },\n \"m.relates_to\": { rel_type: \"m.replace\", event_id: targetEventId },\n };\n}\n","import { createMirror, SILENT, type Mirror, type MirrorBus, type MirrorOptions } from \"./mirror.js\";\nimport { syncFilter } from \"./render.js\";\nimport { DEFAULT_BACKOFF, abortableSleep, superviseLoop } from \"./supervise.js\";\nimport type { MirrorState, QueueSource } from \"./types.js\";\n\ntype Body = (reset: () => void) => Promise<void>;\n\nfunction streamEnded(name: string, signal: AbortSignal): void {\n if (!signal.aborted) throw new Error(`${name} stream ended`);\n}\n\n// Opens the tail before reconciling, so an item opened during reconcile arrives through the tail.\nfunction sourceLoop(mirror: Mirror, source: QueueSource, state: MirrorState, signal: AbortSignal): Body {\n return async (reset) => {\n const events = source.tail(state.sourceCursor(), signal);\n await mirror.reconcile();\n for await (const event of events) {\n await mirror.applySourceEvent(event);\n reset();\n }\n streamEnded(\"source\", signal);\n };\n}\n\nfunction syncLoop(mirror: Mirror, bus: MirrorBus, state: MirrorState, options: MirrorOptions): Body {\n const { roomId, signal, syncTimeoutMs = 30_000 } = options;\n return async (reset) => {\n for await (const batch of bus.syncLoop({ since: state.syncToken(), filter: syncFilter(roomId), timeoutMs: syncTimeoutMs, signal })) {\n await mirror.applySyncBatch(batch);\n reset();\n }\n streamEnded(\"sync\", signal);\n };\n}\n\nfunction sweepLoop(mirror: Mirror, options: MirrorOptions, sleep: NonNullable<MirrorOptions[\"sleep\"]>): Body {\n const { signal, sweepIntervalMs = 5_000 } = options;\n return async (reset) => {\n while (!signal.aborted) {\n await mirror.sweepExpired();\n reset();\n await sleep(sweepIntervalMs, signal);\n }\n };\n}\n\n/** Runs the source tail (reconciling on each start), the sync loop and the expiry sweep until `signal` aborts. */\nexport async function runMirror(source: QueueSource, bus: MirrorBus, state: MirrorState, options: MirrorOptions): Promise<void> {\n const mirror = createMirror(source, bus, state, options);\n const sleep = options.sleep ?? abortableSleep;\n const supervise = { signal: options.signal, backoff: options.backoff ?? DEFAULT_BACKOFF, sleep, logger: options.logger ?? SILENT };\n await Promise.all([\n superviseLoop(\"source\", sourceLoop(mirror, source, state, options.signal), supervise),\n superviseLoop(\"sync\", syncLoop(mirror, bus, state, options), supervise),\n superviseLoop(\"sweep\", sweepLoop(mirror, options, sleep), supervise),\n ]);\n}\n"],"mappings":"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","names":["ITEM_KEY","encodeItem","encodeItem","ITEM_KEY"]}
|
package/dist/sqlite.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Db, Migration } from '@titan-design/store-sqlite';
|
|
2
|
-
import { M as MirrorState, P as PostedItem, b as StateChange } from './types-
|
|
2
|
+
import { M as MirrorState, P as PostedItem, b as StateChange } from './types-CYi2j1Jz.js';
|
|
3
3
|
import '@titan-design/matrix-bus';
|
|
4
4
|
|
|
5
5
|
declare const DEFAULT_MIRROR_TABLE_PREFIX = "queue_mirror";
|
|
@@ -26,6 +26,11 @@ type SourceEvent = {
|
|
|
26
26
|
outcome: CloseOutcome;
|
|
27
27
|
label?: string;
|
|
28
28
|
cursor: string;
|
|
29
|
+
}
|
|
30
|
+
/** The source lost its place (e.g. a gap too large to replay); the mirror re-reconciles against open(). */
|
|
31
|
+
| {
|
|
32
|
+
type: "resync";
|
|
33
|
+
cursor: string;
|
|
29
34
|
};
|
|
30
35
|
interface VerdictInput {
|
|
31
36
|
verdict: Verdict;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@titan-design/queue-mirror",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Projects a local queue of human-actionable items into a Matrix room and folds the owner's verdicts back",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@titan-design/hitl": "^0.2.1",
|
|
43
|
-
"@titan-design/matrix-bus": "^0.
|
|
43
|
+
"@titan-design/matrix-bus": "^0.2.0",
|
|
44
44
|
"@titan-design/store-sqlite": "^0.3.1"
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|