@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 +0 -1
- package/package.json +1 -3
- package/src/callback-handler.js +1 -1
- package/src/callback-registry.js +1 -1
- package/src/dispatcher.js +1 -1
- package/src/index.js +10 -2
- package/src/resume-scheduler.js +7 -17
- package/src/discussion-context.js +0 -126
- package/src/origin-index.js +0 -49
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forwardimpact/libbridge",
|
|
3
|
-
"version": "0.1.
|
|
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"
|
package/src/callback-handler.js
CHANGED
|
@@ -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("./
|
|
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
|
package/src/callback-registry.js
CHANGED
|
@@ -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
|
|
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("./
|
|
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,
|
package/src/resume-scheduler.js
CHANGED
|
@@ -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("./
|
|
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
|
-
|
|
141
|
-
for (const
|
|
142
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
}
|
package/src/origin-index.js
DELETED
|
@@ -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
|
-
}
|