@titan-design/queue-mirror 0.3.1 → 0.4.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 CHANGED
@@ -8,14 +8,19 @@ close edits the item with `m.replace`.
8
8
  - `runMirror(source, bus, state, options)`: the supervised loop.
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
+ Each resolve records the reacting owner as `{ class: "owner-remote", id: sender, channel:
12
+ "matrix", confirmEvent }` (override with `resolverOf`); a refused resolver leaves the item
13
+ open and marked refused.
14
+ - `@titan-design/queue-mirror/sqlite`: `SqliteMirrorState`, the durable `MirrorState`, and
15
+ `mirrorMigration(version, prefix)` for a product's own migration list.
11
16
 
12
17
  `runMirror` calls `source.tail()` before `reconcile()`, so a `QueueSource.tail` must connect
13
18
  eagerly: the connection must exist when `tail()` returns, not at the first `next()`. A lazy
14
19
  `async function*` misses items opened during reconcile. A source that cannot replay a gap
15
20
  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.
21
+ the cursor. `VerdictInput.sender` names the Matrix user who sent the resolving event. A `rejected` verdict edits the item `refused: <detail>` and leaves it open.
17
22
 
18
- Tier 2 of the titan-platform DAG. Depends on `@titan-design/matrix-bus` and
19
- `@titan-design/hitl`. The root entry has no `node:` import.
23
+ Tier 2 of the titan-platform DAG. Depends on `@titan-design/matrix-bus`,
24
+ `@titan-design/hitl` and `@titan-design/store-sqlite`. The root entry has no `node:` import.
20
25
 
21
26
  Reference: [site/reference/queue-mirror.md](../../site/reference/queue-mirror.md).
package/dist/hitl.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { GateRecord, GateStore } from '@titan-design/hitl';
2
- import { V as VerdictInput, Q as QueueSource } from './types-CYi2j1Jz.js';
1
+ import { GateRecord, GateResolver, GateStore } from '@titan-design/hitl';
2
+ import { V as VerdictInput, Q as QueueSource } from './types-CycIozyj.js';
3
3
  import '@titan-design/matrix-bus';
4
4
 
5
5
  type GateKind = "approval_request" | "question";
@@ -16,10 +16,14 @@ interface HitlSourceOptions {
16
16
  pollMs?: number;
17
17
  kindOf?: (gate: GateRecord) => GateKind;
18
18
  toPayload?: (gate: GateRecord, verdict: VerdictInput) => GateAction;
19
+ /** Who the store records as resolving the gate; defaults to the owner over Matrix. */
20
+ resolverOf?: (gate: GateRecord, verdict: VerdictInput) => GateResolver;
19
21
  }
20
22
  /** allow and approve resolve {approved: true}; an answer resolves its text; deny and dismiss cancel. */
21
23
  declare function defaultGatePayload(_gate: GateRecord, { verdict, text }: VerdictInput): GateAction;
24
+ /** The reacting sender, which the mirror's fold has already checked is the owner. */
25
+ declare function defaultGateResolver(_gate: GateRecord, { sender, resolutionEventId }: VerdictInput): GateResolver;
22
26
  /** A QueueSource over a hitl GateStore: pending gates become approval_request or question items. */
23
27
  declare function hitlQueueSource(store: GateStore, options: HitlSourceOptions): QueueSource;
24
28
 
25
- export { type GateAction, type HitlSourceOptions, defaultGatePayload, hitlQueueSource };
29
+ export { type GateAction, type HitlSourceOptions, defaultGatePayload, defaultGateResolver, hitlQueueSource };
package/dist/hitl.js CHANGED
@@ -12,13 +12,21 @@ function defaultGatePayload(_gate, { verdict, text }) {
12
12
  if (verdict === "answer") return { resolve: text };
13
13
  return { cancel: "denied from Matrix" };
14
14
  }
15
+ function defaultGateResolver(_gate, { sender, resolutionEventId }) {
16
+ return { class: "owner-remote", id: sender, channel: "matrix", confirmEvent: resolutionEventId };
17
+ }
15
18
  function errorResult(err) {
16
19
  const name = err instanceof Error ? err.name : "";
17
20
  const detail = err instanceof Error ? err.message : String(err);
18
21
  if (CLOSED_ERRORS.has(name)) return { ok: false, reason: "closed", detail };
19
22
  if (name === "GatePayloadInvalid") return { ok: false, reason: "rejected", detail };
23
+ if (name === "GateResolverRefused") return { ok: false, reason: "rejected", detail: refusalDetail(err) };
20
24
  throw err;
21
25
  }
26
+ function refusalDetail(err) {
27
+ const actorClass = err.actorClass;
28
+ return typeof actorClass === "string" ? `resolver ${actorClass} refused` : "resolver refused";
29
+ }
22
30
  var HitlQueueSource = class {
23
31
  constructor(store, options) {
24
32
  this.store = store;
@@ -35,7 +43,7 @@ var HitlQueueSource = class {
35
43
  if (gate?.status !== "pending") return { ok: false, reason: "closed" };
36
44
  const action = (this.options.toPayload ?? defaultGatePayload)(gate, verdict);
37
45
  try {
38
- if ("resolve" in action) this.store.resolve(id, action.resolve);
46
+ if ("resolve" in action) this.store.resolve(id, action.resolve, (this.options.resolverOf ?? defaultGateResolver)(gate, verdict));
39
47
  else this.store.cancel(id, action.cancel);
40
48
  return { ok: true };
41
49
  } catch (err) {
@@ -78,6 +86,7 @@ function hitlQueueSource(store, options) {
78
86
  }
79
87
  export {
80
88
  defaultGatePayload,
89
+ defaultGateResolver,
81
90
  hitlQueueSource
82
91
  };
83
92
  //# sourceMappingURL=hitl.js.map
package/dist/hitl.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/hitl.ts"],"sourcesContent":["import type { GateRecord, GateStatus, GateStore } from \"@titan-design/hitl\";\nimport { abortableSleep } from \"./supervise.js\";\nimport type { CloseOutcome, QueueItem, QueueSource, ResolveResult, SourceEvent, VerdictInput } from \"./types.js\";\n\ntype GateKind = \"approval_request\" | \"question\";\nexport type GateAction = { resolve: unknown } | { cancel: string };\n\nexport interface HitlSourceOptions {\n /** TitanItem headline fields. */\n machine: string;\n session: string;\n /** hitl has no change feed, so tail polls listPending at this interval. */\n pollMs?: number;\n kindOf?: (gate: GateRecord) => GateKind;\n toPayload?: (gate: GateRecord, verdict: VerdictInput) => GateAction;\n}\n\nconst DEFAULT_POLL_MS = 1_000;\n// Matched by name, so a second hitl copy in node_modules cannot break instanceof.\nconst CLOSED_ERRORS = new Set([\"GateAlreadySettled\", \"GateExpired\", \"GateNotFound\"]);\nconst OUTCOMES: Record<Exclude<GateStatus, \"pending\">, CloseOutcome> = { resolved: \"resolved\", cancelled: \"cancelled\", expired: \"expired\" };\n\nconst defaultKind = (gate: GateRecord): GateKind => (gate.schema ? \"question\" : \"approval_request\");\n\n/** allow and approve resolve {approved: true}; an answer resolves its text; deny and dismiss cancel. */\nexport function defaultGatePayload(_gate: GateRecord, { verdict, text }: VerdictInput): GateAction {\n if (verdict === \"allow\" || verdict === \"approve\") return { resolve: { approved: true } };\n if (verdict === \"answer\") return { resolve: text };\n return { cancel: \"denied from Matrix\" };\n}\n\nfunction errorResult(err: unknown): ResolveResult {\n const name = err instanceof Error ? err.name : \"\";\n const detail = err instanceof Error ? err.message : String(err);\n if (CLOSED_ERRORS.has(name)) return { ok: false, reason: \"closed\", detail };\n if (name === \"GatePayloadInvalid\") return { ok: false, reason: \"rejected\", detail };\n throw err;\n}\n\nclass HitlQueueSource implements QueueSource {\n readonly kinds = [\"approval_request\", \"question\"] as const;\n\n constructor(\n private readonly store: GateStore,\n private readonly options: HitlSourceOptions,\n ) {}\n\n async open(): Promise<QueueItem[]> {\n return this.store.listPending().map((gate) => this.toItem(gate));\n }\n\n async resolve(id: string, verdict: VerdictInput): Promise<ResolveResult> {\n const gate = this.store.get(id);\n if (gate?.status !== \"pending\") return { ok: false, reason: \"closed\" };\n const action = (this.options.toPayload ?? defaultGatePayload)(gate, verdict);\n try {\n if (\"resolve\" in action) this.store.resolve(id, action.resolve);\n else this.store.cancel(id, action.cancel);\n return { ok: true };\n } catch (err) {\n return errorResult(err);\n }\n }\n\n /** The cursor only numbers events; after a restart, reconcile over open() is what catches up. */\n async *tail(_cursor: string | undefined, signal: AbortSignal): AsyncIterable<SourceEvent> {\n const known = new Set<string>();\n let seq = 0;\n while (!signal.aborted) {\n for (const event of this.diff(known)) yield { ...event, cursor: String(++seq) } as SourceEvent;\n await abortableSleep(this.options.pollMs ?? DEFAULT_POLL_MS, signal);\n }\n }\n\n private diff(known: Set<string>): Array<Omit<SourceEvent, \"cursor\">> {\n const pending = this.store.listPending();\n const pendingIds = new Set(pending.map((gate) => gate.id));\n const closed = [...known].filter((id) => !pendingIds.has(id)).map((id) => this.closedEvent(id));\n const opened = pending.filter((gate) => !known.has(gate.id)).map((gate) => ({ type: \"opened\" as const, item: this.toItem(gate) }));\n for (const id of pendingIds) known.add(id);\n for (const event of closed) known.delete(event.id);\n return [...closed, ...opened];\n }\n\n private closedEvent(id: string): { type: \"closed\"; id: string; outcome: CloseOutcome; label?: string } {\n const gate = this.store.get(id);\n const outcome = gate && gate.status !== \"pending\" ? OUTCOMES[gate.status] : \"cancelled\";\n return { type: \"closed\", id, outcome, label: gate?.reason };\n }\n\n private toItem(gate: GateRecord): QueueItem {\n const kind = (this.options.kindOf ?? defaultKind)(gate);\n const { machine, session } = this.options;\n const prompt = kind === \"approval_request\" ? { toolName: \"gate\", inputPreview: gate.prompt } : { text: gate.prompt };\n const expiresAt = gate.expiresAt === undefined ? undefined : Date.parse(gate.expiresAt);\n return { id: gate.id, kind, machine, session, at: Date.parse(gate.createdAt), expiresAt, ...prompt };\n }\n}\n\n/** A QueueSource over a hitl GateStore: pending gates become approval_request or question items. */\nexport function hitlQueueSource(store: GateStore, options: HitlSourceOptions): QueueSource {\n return new HitlQueueSource(store, options);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/hitl.ts"],"sourcesContent":["import type { GateRecord, GateResolver, GateStatus, GateStore } from \"@titan-design/hitl\";\nimport { abortableSleep } from \"./supervise.js\";\nimport type { CloseOutcome, QueueItem, QueueSource, ResolveResult, SourceEvent, VerdictInput } from \"./types.js\";\n\ntype GateKind = \"approval_request\" | \"question\";\nexport type GateAction = { resolve: unknown } | { cancel: string };\n\nexport interface HitlSourceOptions {\n /** TitanItem headline fields. */\n machine: string;\n session: string;\n /** hitl has no change feed, so tail polls listPending at this interval. */\n pollMs?: number;\n kindOf?: (gate: GateRecord) => GateKind;\n toPayload?: (gate: GateRecord, verdict: VerdictInput) => GateAction;\n /** Who the store records as resolving the gate; defaults to the owner over Matrix. */\n resolverOf?: (gate: GateRecord, verdict: VerdictInput) => GateResolver;\n}\n\nconst DEFAULT_POLL_MS = 1_000;\n// Matched by name, so a second hitl copy in node_modules cannot break instanceof.\nconst CLOSED_ERRORS = new Set([\"GateAlreadySettled\", \"GateExpired\", \"GateNotFound\"]);\nconst OUTCOMES: Record<Exclude<GateStatus, \"pending\">, CloseOutcome> = { resolved: \"resolved\", cancelled: \"cancelled\", expired: \"expired\" };\n\nconst defaultKind = (gate: GateRecord): GateKind => (gate.schema ? \"question\" : \"approval_request\");\n\n/** allow and approve resolve {approved: true}; an answer resolves its text; deny and dismiss cancel. */\nexport function defaultGatePayload(_gate: GateRecord, { verdict, text }: VerdictInput): GateAction {\n if (verdict === \"allow\" || verdict === \"approve\") return { resolve: { approved: true } };\n if (verdict === \"answer\") return { resolve: text };\n return { cancel: \"denied from Matrix\" };\n}\n\n/** The reacting sender, which the mirror's fold has already checked is the owner. */\nexport function defaultGateResolver(_gate: GateRecord, { sender, resolutionEventId }: VerdictInput): GateResolver {\n return { class: \"owner-remote\", id: sender, channel: \"matrix\", confirmEvent: resolutionEventId };\n}\n\n/**\n * Anything unmatched, such as GateAuthorizeInvalid or GateStoreSchemaOutdated, is a\n * configuration fault: it is rethrown so the supervisor logs it and the verdict is retried.\n */\nfunction errorResult(err: unknown): ResolveResult {\n const name = err instanceof Error ? err.name : \"\";\n const detail = err instanceof Error ? err.message : String(err);\n if (CLOSED_ERRORS.has(name)) return { ok: false, reason: \"closed\", detail };\n if (name === \"GatePayloadInvalid\") return { ok: false, reason: \"rejected\", detail };\n if (name === \"GateResolverRefused\") return { ok: false, reason: \"rejected\", detail: refusalDetail(err) };\n throw err;\n}\n\n// The refusal reason can come from a caller's authorize, so only the actor class reaches the room.\nfunction refusalDetail(err: unknown): string {\n const actorClass = (err as { actorClass?: unknown }).actorClass;\n return typeof actorClass === \"string\" ? `resolver ${actorClass} refused` : \"resolver refused\";\n}\n\nclass HitlQueueSource implements QueueSource {\n readonly kinds = [\"approval_request\", \"question\"] as const;\n\n constructor(\n private readonly store: GateStore,\n private readonly options: HitlSourceOptions,\n ) {}\n\n async open(): Promise<QueueItem[]> {\n return this.store.listPending().map((gate) => this.toItem(gate));\n }\n\n async resolve(id: string, verdict: VerdictInput): Promise<ResolveResult> {\n const gate = this.store.get(id);\n if (gate?.status !== \"pending\") return { ok: false, reason: \"closed\" };\n const action = (this.options.toPayload ?? defaultGatePayload)(gate, verdict);\n try {\n if (\"resolve\" in action) this.store.resolve(id, action.resolve, (this.options.resolverOf ?? defaultGateResolver)(gate, verdict));\n else this.store.cancel(id, action.cancel);\n return { ok: true };\n } catch (err) {\n return errorResult(err);\n }\n }\n\n /** The cursor only numbers events; after a restart, reconcile over open() is what catches up. */\n async *tail(_cursor: string | undefined, signal: AbortSignal): AsyncIterable<SourceEvent> {\n const known = new Set<string>();\n let seq = 0;\n while (!signal.aborted) {\n for (const event of this.diff(known)) yield { ...event, cursor: String(++seq) } as SourceEvent;\n await abortableSleep(this.options.pollMs ?? DEFAULT_POLL_MS, signal);\n }\n }\n\n private diff(known: Set<string>): Array<Omit<SourceEvent, \"cursor\">> {\n const pending = this.store.listPending();\n const pendingIds = new Set(pending.map((gate) => gate.id));\n const closed = [...known].filter((id) => !pendingIds.has(id)).map((id) => this.closedEvent(id));\n const opened = pending.filter((gate) => !known.has(gate.id)).map((gate) => ({ type: \"opened\" as const, item: this.toItem(gate) }));\n for (const id of pendingIds) known.add(id);\n for (const event of closed) known.delete(event.id);\n return [...closed, ...opened];\n }\n\n private closedEvent(id: string): { type: \"closed\"; id: string; outcome: CloseOutcome; label?: string } {\n const gate = this.store.get(id);\n const outcome = gate && gate.status !== \"pending\" ? OUTCOMES[gate.status] : \"cancelled\";\n return { type: \"closed\", id, outcome, label: gate?.reason };\n }\n\n private toItem(gate: GateRecord): QueueItem {\n const kind = (this.options.kindOf ?? defaultKind)(gate);\n const { machine, session } = this.options;\n const prompt = kind === \"approval_request\" ? { toolName: \"gate\", inputPreview: gate.prompt } : { text: gate.prompt };\n const expiresAt = gate.expiresAt === undefined ? undefined : Date.parse(gate.expiresAt);\n return { id: gate.id, kind, machine, session, at: Date.parse(gate.createdAt), expiresAt, ...prompt };\n }\n}\n\n/** A QueueSource over a hitl GateStore: pending gates become approval_request or question items. */\nexport function hitlQueueSource(store: GateStore, options: HitlSourceOptions): QueueSource {\n return new HitlQueueSource(store, options);\n}\n"],"mappings":"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","names":[]}
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-CYi2j1Jz.js';
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-CycIozyj.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
@@ -325,7 +325,7 @@ var QueueMirror = class {
325
325
  async applyResolution(event, { itemEventId, verdict, text }) {
326
326
  const item = this.state.byEventId(itemEventId);
327
327
  const applied = [event.event_id];
328
- const result = await this.resolveAtSource(item.sourceId, { verdict, text, resolutionEventId: event.event_id });
328
+ const result = await this.resolveAtSource(item.sourceId, { verdict, text, resolutionEventId: event.event_id, sender: event.sender });
329
329
  if (result.ok) return this.close(item, `resolved: ${verdict}`, { applied });
330
330
  if (result.reason === "closed") return this.close(item, "already resolved", { applied });
331
331
  await this.markRefused(item, event.event_id, result.detail);
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// 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"]}
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, sender: event.sender });\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-CYi2j1Jz.js';
2
+ import { M as MirrorState, P as PostedItem, b as StateChange } from './types-CycIozyj.js';
3
3
  import '@titan-design/matrix-bus';
4
4
 
5
5
  declare const DEFAULT_MIRROR_TABLE_PREFIX = "queue_mirror";
@@ -36,6 +36,8 @@ interface VerdictInput {
36
36
  verdict: Verdict;
37
37
  text?: string;
38
38
  resolutionEventId: string;
39
+ /** The Matrix user who sent the resolving event; the fold only admits the owner. */
40
+ sender: string;
39
41
  }
40
42
  type ResolveResult = {
41
43
  ok: true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@titan-design/queue-mirror",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
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",
@@ -39,7 +39,7 @@
39
39
  "access": "public"
40
40
  },
41
41
  "dependencies": {
42
- "@titan-design/hitl": "^0.2.1",
42
+ "@titan-design/hitl": "^0.3.0",
43
43
  "@titan-design/matrix-bus": "^0.2.0",
44
44
  "@titan-design/store-sqlite": "^0.3.1"
45
45
  },