@forwardimpact/libbridge 0.1.4 → 0.1.5

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
@@ -13,7 +13,6 @@ Threaded-channel bridge primitives — relay messages between human channels
13
13
  import {
14
14
  createBridgeServer,
15
15
  CallbackRegistry,
16
- DiscussionContextStore,
17
16
  RateLimiter,
18
17
  ProgressTicker,
19
18
  appendHistory,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forwardimpact/libbridge",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Threaded-channel bridge primitives — relay messages between human channels (GitHub Discussions, Microsoft Teams) and the Kata agent team.",
5
5
  "keywords": [
6
6
  "bridge",
@@ -40,8 +40,6 @@
40
40
  "test": "bun test test/*.test.js"
41
41
  },
42
42
  "dependencies": {
43
- "@forwardimpact/libindex": "^0.1.38",
44
- "@forwardimpact/libstorage": "^0.1.78",
45
43
  "@forwardimpact/libtype": "^0.1.0",
46
44
  "@hono/node-server": "^2.0.4",
47
45
  "hono": "^4.12.23"
@@ -37,7 +37,7 @@ export class CallbackHandlerError extends Error {
37
37
  * @param {string} options.channel
38
38
  * @param {import("./callback-registry.js").CallbackRegistry} options.callbacks
39
39
  * @param {import("./acknowledgement.js").Acknowledgement} options.ack
40
- * @param {import("./discussion-context.js").DiscussionContextStore} options.store
40
+ * @param {import("./index.js").DiscussionAdapter} options.store
41
41
  * @param {{debug?: Function, error?: Function}} options.logger
42
42
  * @param {{startSpan: Function}} options.tracer
43
43
  * @param {string} options.spanName
@@ -4,7 +4,7 @@ const DEFAULT_TTL_MS = 2 * 60 * 60 * 1000;
4
4
 
5
5
  /**
6
6
  * In-memory registry of pending bridge → workflow callbacks. Hosts persist
7
- * the (token, correlationId) pairs via DiscussionContextStore so the registry
7
+ * the (token, correlationId) pairs via the discussion store so the registry
8
8
  * can be rehydrated after a restart; this class only owns the live token →
9
9
  * metadata mapping and TTL sweep.
10
10
  */
package/src/dispatcher.js CHANGED
@@ -17,7 +17,7 @@ export class Dispatcher {
17
17
  * @param {object} options
18
18
  * @param {import("./callback-registry.js").CallbackRegistry} options.callbacks
19
19
  * @param {import("./acknowledgement.js").Acknowledgement} options.ack
20
- * @param {import("./discussion-context.js").DiscussionContextStore} options.store
20
+ * @param {import("./index.js").DiscussionAdapter} options.store
21
21
  * @param {string} options.callbackBaseUrl - Already normalised
22
22
  * @param {string} options.workflowFile
23
23
  * @param {string} options.githubRepo
package/src/index.js CHANGED
@@ -10,14 +10,22 @@
10
10
  * @property {string} github_repo - "owner/repo" hosting the kata-dispatch workflow
11
11
  */
12
12
 
13
+ /**
14
+ * @typedef {object} DiscussionAdapter
15
+ * @property {(channel: string, discussionId: string) => Promise<object|null>} loadByChannel
16
+ * @property {(correlationId: string) => Promise<object|null>} loadByCorrelation
17
+ * @property {() => Promise<Array<{correlationId: string, dueAt: number}>>} listOpenRecesses
18
+ * @property {(ctx: object) => Promise<void>} add
19
+ * @property {() => Promise<void>} flush
20
+ * @property {() => Promise<void>} shutdown
21
+ */
22
+
13
23
  export { createBridgeServer } from "./server.js";
14
24
  export { CallbackRegistry } from "./callback-registry.js";
15
25
  export { buildPrompt } from "./prompt.js";
16
26
  export { appendHistory } from "./history.js";
17
27
  export { RateLimiter } from "./rate-limit.js";
18
28
  export { dispatchWorkflow } from "./dispatch.js";
19
- export { DiscussionContextStore } from "./discussion-context.js";
20
- export { OriginIndex } from "./origin-index.js";
21
29
  export { ProgressTicker } from "./progress-ticker.js";
22
30
  export {
23
31
  Acknowledgement,
@@ -17,7 +17,7 @@ export class ResumeScheduler {
17
17
  /**
18
18
  * @param {object} options
19
19
  * @param {import("./dispatcher.js").Dispatcher} options.dispatcher
20
- * @param {import("./discussion-context.js").DiscussionContextStore} options.store
20
+ * @param {import("./index.js").DiscussionAdapter} options.store
21
21
  * @param {{error?: Function, info?: Function}} [options.logger]
22
22
  * @param {string} [options.prompt] - Default "Resume requested."
23
23
  * @param {(ctx: object) => object} [options.buildCallbackMeta]
@@ -137,15 +137,9 @@ export class ResumeScheduler {
137
137
  * @returns {Promise<void>}
138
138
  */
139
139
  async rearm() {
140
- if (!this.#store.loaded) await this.#store.loadData();
141
- for (const record of this.#store.index.values()) {
142
- const open = record?.open_rfcs;
143
- if (!open) continue;
144
- for (const [correlationId, rfc] of Object.entries(open)) {
145
- if (typeof rfc.due_at === "number") {
146
- this.#elapsed.schedule(correlationId, rfc.due_at);
147
- }
148
- }
140
+ const refs = await this.#store.listOpenRecesses();
141
+ for (const { correlationId, dueAt } of refs) {
142
+ this.#elapsed.schedule(correlationId, dueAt);
149
143
  }
150
144
  }
151
145
 
@@ -220,12 +214,8 @@ export class ResumeScheduler {
220
214
  }
221
215
 
222
216
  async #findContextWithRfc(correlationId) {
223
- if (!this.#store.loaded) await this.#store.loadData();
224
- for (const record of this.#store.index.values()) {
225
- if (record?.open_rfcs?.[correlationId]) {
226
- return { ctx: record, rfc: record.open_rfcs[correlationId] };
227
- }
228
- }
229
- return null;
217
+ const ctx = await this.#store.loadByCorrelation(correlationId);
218
+ if (!ctx) return null;
219
+ return { ctx, rfc: ctx.open_rfcs[correlationId] };
230
220
  }
231
221
  }
@@ -1,126 +0,0 @@
1
- import { BufferedIndex } from "@forwardimpact/libindex";
2
-
3
- const DEFAULT_FLUSH_INTERVAL_MS = 5_000;
4
- const DEFAULT_MAX_BUFFER_SIZE = 1_000;
5
- const DEFAULT_CONVERSATION_TTL_MS = 24 * 60 * 60 * 1000;
6
- const DEFAULT_SWEEP_INTERVAL_MS = 60_000;
7
-
8
- /**
9
- * Persisted thread state keyed by `(channel, discussion_id)`. Both
10
- * `services/ghbridge` and `services/msbridge` write into the same store so
11
- * the channel-agnostic `kata-dispatch.yml` workflow can resume conversations
12
- * from either side.
13
- *
14
- * Record shape:
15
- * {
16
- * id: "<channel>:<discussion_id>",
17
- * channel: "github-discussions" | "msteams",
18
- * discussion_id: string,
19
- * history: Array<{role: "user"|"assistant", text: string}>,
20
- * participants: Array<{name, kind: "agent"|"human", external_id?, metadata?}>,
21
- * open_rfcs: Record<correlationId, {trigger, opened_at, history_index_at_open}>,
22
- * lead: string,
23
- * pending_callbacks: Record<token, correlationId>,
24
- * last_active_at: number,
25
- * }
26
- *
27
- * @augments BufferedIndex
28
- */
29
- export class DiscussionContextStore extends BufferedIndex {
30
- #conversationTtlMs;
31
- #sweepTimer;
32
-
33
- /**
34
- * @param {import("@forwardimpact/libstorage").StorageInterface} storage
35
- * @param {object} [options]
36
- * @param {string} [options.indexKey] - JSONL file name (default `discussions.jsonl`)
37
- * @param {number} [options.flushIntervalMs]
38
- * @param {number} [options.maxBufferSize]
39
- * @param {number} [options.conversationTtlMs] - Eviction window (default 24h)
40
- * @param {number} [options.sweepIntervalMs] - Sweep cadence (default 60s)
41
- */
42
- constructor(
43
- storage,
44
- {
45
- indexKey = "discussions.jsonl",
46
- flushIntervalMs = DEFAULT_FLUSH_INTERVAL_MS,
47
- maxBufferSize = DEFAULT_MAX_BUFFER_SIZE,
48
- conversationTtlMs = DEFAULT_CONVERSATION_TTL_MS,
49
- sweepIntervalMs = DEFAULT_SWEEP_INTERVAL_MS,
50
- } = {},
51
- ) {
52
- super(storage, indexKey, {
53
- flush_interval: flushIntervalMs,
54
- max_buffer_size: maxBufferSize,
55
- });
56
- this.#conversationTtlMs = conversationTtlMs;
57
- this.#sweepTimer = setInterval(
58
- () => this.#sweep(Date.now()),
59
- sweepIntervalMs,
60
- );
61
- this.#sweepTimer.unref();
62
- }
63
-
64
- /**
65
- * Compose the `id` field for a `(channel, discussion_id)` pair.
66
- * @param {string} channel
67
- * @param {string} discussionId
68
- * @returns {string}
69
- */
70
- static keyOf(channel, discussionId) {
71
- return `${channel}:${discussionId}`;
72
- }
73
-
74
- /**
75
- * @param {string} channel
76
- * @param {string} discussionId
77
- * @returns {Promise<object | null>}
78
- */
79
- async loadByChannel(channel, discussionId) {
80
- if (!this.loaded) await this.loadData();
81
- const id = DiscussionContextStore.keyOf(channel, discussionId);
82
- return this.index.get(id) ?? null;
83
- }
84
-
85
- /**
86
- * Stop the periodic sweep timer. Called on host shutdown alongside
87
- * `shutdown()` to release the interval.
88
- */
89
- stopSweep() {
90
- if (this.#sweepTimer) {
91
- clearInterval(this.#sweepTimer);
92
- this.#sweepTimer = null;
93
- }
94
- }
95
-
96
- /**
97
- * Flush buffered writes and stop the sweep timer.
98
- * @returns {Promise<void>}
99
- */
100
- async shutdown() {
101
- this.stopSweep();
102
- await super.shutdown();
103
- }
104
-
105
- /**
106
- * Evict records whose `last_active_at` is older than `conversationTtlMs`.
107
- * Caller-driven `now` keeps unit tests deterministic.
108
- * @param {number} now
109
- * @returns {number}
110
- */
111
- sweepNow(now) {
112
- return this.#sweep(now);
113
- }
114
-
115
- #sweep(now) {
116
- let evicted = 0;
117
- for (const [id, record] of this.index) {
118
- const lastActive = record?.last_active_at ?? 0;
119
- if (now - lastActive > this.#conversationTtlMs) {
120
- this.index.delete(id);
121
- evicted++;
122
- }
123
- }
124
- return evicted;
125
- }
126
- }
@@ -1,49 +0,0 @@
1
- import { BufferedIndex } from "@forwardimpact/libindex";
2
-
3
- const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
4
-
5
- /**
6
- * Tracks comment IDs posted by the bridge so webhook handlers can skip
7
- * dispatching for self-originated events. Wraps `BufferedIndex` with
8
- * caller-injected `StorageInterface` per the libbridge invariant.
9
- *
10
- * Record shape: `{ id: "<comment_node_id>", discussion_id, posted_at }`
11
- *
12
- * @augments BufferedIndex
13
- */
14
- export class OriginIndex extends BufferedIndex {
15
- #ttlMs;
16
-
17
- /**
18
- * @param {import("@forwardimpact/libstorage").StorageInterface} storage
19
- * @param {object} [options]
20
- * @param {string} [options.indexKey]
21
- * @param {number} [options.ttlMs] - Eviction window (default 24h)
22
- */
23
- constructor(
24
- storage,
25
- { indexKey = "origins.jsonl", ttlMs = DEFAULT_TTL_MS } = {},
26
- ) {
27
- super(storage, indexKey, {
28
- flush_interval: 1_000,
29
- max_buffer_size: 100,
30
- });
31
- this.#ttlMs = ttlMs;
32
- }
33
-
34
- /**
35
- * Evict records older than `ttlMs`.
36
- * @param {number} now
37
- * @returns {number} count evicted
38
- */
39
- sweep(now) {
40
- let evicted = 0;
41
- for (const [id, record] of this.index) {
42
- if (now - (record?.posted_at ?? 0) > this.#ttlMs) {
43
- this.index.delete(id);
44
- evicted++;
45
- }
46
- }
47
- return evicted;
48
- }
49
- }