@holon-run/agentinbox 0.1.1 → 0.1.4
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 +46 -1
- package/dist/src/cli.js +6 -1
- package/dist/src/sources/feishu.js +1 -225
- package/dist/src/sources/github.js +2 -237
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -16,6 +16,43 @@ In practice, that means `AgentInbox` can:
|
|
|
16
16
|
- wake or drive agent sessions running in `tmux` or `iTerm2`, even when the
|
|
17
17
|
agent runtime does not expose a notification API
|
|
18
18
|
|
|
19
|
+
## Event Flow
|
|
20
|
+
|
|
21
|
+
At a high level, `AgentInbox` sits between external events and the current
|
|
22
|
+
agent session:
|
|
23
|
+
|
|
24
|
+

|
|
25
|
+
|
|
26
|
+
## Review Workflow
|
|
27
|
+
|
|
28
|
+
One concrete workflow is reviewer / developer collaboration around a PR:
|
|
29
|
+
|
|
30
|
+
```mermaid
|
|
31
|
+
sequenceDiagram
|
|
32
|
+
participant D as Developer Agent
|
|
33
|
+
participant G as GitHub / CI
|
|
34
|
+
participant A as AgentInbox
|
|
35
|
+
participant R as Reviewer Agent
|
|
36
|
+
|
|
37
|
+
D->>G: Open PR / Push fix
|
|
38
|
+
G->>A: PR opened / CI started
|
|
39
|
+
A->>R: Wake reviewer
|
|
40
|
+
|
|
41
|
+
R->>G: Leave review comment
|
|
42
|
+
G->>A: Review comment created
|
|
43
|
+
A->>D: Wake developer
|
|
44
|
+
|
|
45
|
+
D->>G: Push fix
|
|
46
|
+
G->>A: CI completed
|
|
47
|
+
A->>D: Wake developer
|
|
48
|
+
A->>R: Wake reviewer
|
|
49
|
+
|
|
50
|
+
R->>G: Approve / Merge
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`AgentInbox` is what lets both agents stay in the loop without polling GitHub
|
|
54
|
+
manually or relying on the agent runtime to expose its own notification API.
|
|
55
|
+
|
|
19
56
|
## Status
|
|
20
57
|
|
|
21
58
|
`AgentInbox` is public beta software.
|
|
@@ -32,7 +69,8 @@ In practice, that means `AgentInbox` can:
|
|
|
32
69
|
Requires:
|
|
33
70
|
|
|
34
71
|
- Node.js 20 or newer
|
|
35
|
-
- `uxc` 0.
|
|
72
|
+
- `uxc` 0.15.0 or newer if you want to use GitHub or Feishu adapters:
|
|
73
|
+
https://github.com/holon-run/uxc
|
|
36
74
|
|
|
37
75
|
Install globally:
|
|
38
76
|
|
|
@@ -61,6 +99,13 @@ If you are using Codex or Claude Code, start with the bundled AgentInbox skill:
|
|
|
61
99
|
- repo copy: [`skills/agentinbox/SKILL.md`](./skills/agentinbox/SKILL.md)
|
|
62
100
|
- docs site copy: `https://agentinbox.holon.run/skills/agentinbox/SKILL`
|
|
63
101
|
|
|
102
|
+
If you use the community `skills` installer, you can install the bundled skill
|
|
103
|
+
directly:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npx skills add holon-run/agentinbox --skill agentinbox -a codex -a claude-code
|
|
107
|
+
```
|
|
108
|
+
|
|
64
109
|
That skill is the recommended onboarding path. It can guide the agent through:
|
|
65
110
|
|
|
66
111
|
- checking or installing `agentinbox`
|
package/dist/src/cli.js
CHANGED
|
@@ -310,7 +310,8 @@ async function main() {
|
|
|
310
310
|
}
|
|
311
311
|
if (command === "inbox" && normalized[1] === "read") {
|
|
312
312
|
const args = normalized.slice(2);
|
|
313
|
-
|
|
313
|
+
const allowedFlags = ["--agent-id", "--after-item", "--include-acked"];
|
|
314
|
+
if (positionalArgs(args, ["--agent-id", "--after-item"]).length > 0 || unexpectedFlags(args, allowedFlags).length > 0) {
|
|
314
315
|
throw new Error("usage: agentinbox inbox read [--agent-id ID] [--after-item ID] [--include-acked]");
|
|
315
316
|
}
|
|
316
317
|
const selection = await selectAgentForCommand(client, {
|
|
@@ -693,6 +694,10 @@ function positionalArgs(args, flagsWithValues) {
|
|
|
693
694
|
}
|
|
694
695
|
return positionals;
|
|
695
696
|
}
|
|
697
|
+
function unexpectedFlags(args, allowedFlags) {
|
|
698
|
+
const allowed = new Set(allowedFlags);
|
|
699
|
+
return args.filter((token) => token.startsWith("--") && !allowed.has(token));
|
|
700
|
+
}
|
|
696
701
|
function printHelp(path = []) {
|
|
697
702
|
const key = path[0] ?? "root";
|
|
698
703
|
const helpByKey = {
|
|
@@ -1,60 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FeishuDeliveryAdapter = exports.
|
|
3
|
+
exports.FeishuDeliveryAdapter = exports.FeishuUxcClient = exports.DEFAULT_FEISHU_EVENT_TYPES = exports.FEISHU_IM_SCHEMA_URL = exports.FEISHU_OPENAPI_ENDPOINT = void 0;
|
|
4
4
|
exports.normalizeFeishuBotEvent = normalizeFeishuBotEvent;
|
|
5
5
|
exports.parseFeishuSourceConfig = parseFeishuSourceConfig;
|
|
6
6
|
const uxc_daemon_client_1 = require("@holon-run/uxc-daemon-client");
|
|
7
7
|
exports.FEISHU_OPENAPI_ENDPOINT = "https://open.feishu.cn/open-apis";
|
|
8
8
|
exports.FEISHU_IM_SCHEMA_URL = "https://raw.githubusercontent.com/holon-run/uxc/main/skills/feishu-openapi-skill/references/feishu-im.openapi.json";
|
|
9
9
|
exports.DEFAULT_FEISHU_EVENT_TYPES = ["im.message.receive_v1"];
|
|
10
|
-
const DEFAULT_SYNC_INTERVAL_SECS = 2;
|
|
11
|
-
const MAX_ERROR_BACKOFF_MULTIPLIER = 8;
|
|
12
10
|
class FeishuUxcClient {
|
|
13
11
|
client;
|
|
14
12
|
constructor(client = new uxc_daemon_client_1.UxcDaemonClient({ env: process.env })) {
|
|
15
13
|
this.client = client;
|
|
16
14
|
}
|
|
17
|
-
async ensureLongConnectionSubscription(config, checkpoint) {
|
|
18
|
-
if (checkpoint.uxcJobId) {
|
|
19
|
-
try {
|
|
20
|
-
const status = await this.client.subscribeStatus(checkpoint.uxcJobId);
|
|
21
|
-
if (status.status === "running" || status.status === "reconnecting") {
|
|
22
|
-
return {
|
|
23
|
-
job_id: checkpoint.uxcJobId,
|
|
24
|
-
mode: "stream",
|
|
25
|
-
protocol: "feishu_long_connection",
|
|
26
|
-
endpoint: config.endpoint ?? exports.FEISHU_OPENAPI_ENDPOINT,
|
|
27
|
-
sink: "memory:",
|
|
28
|
-
status: status.status,
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
catch {
|
|
33
|
-
// Recreate the job below.
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return this.client.subscribeStart({
|
|
37
|
-
endpoint: config.endpoint ?? exports.FEISHU_OPENAPI_ENDPOINT,
|
|
38
|
-
mode: "stream",
|
|
39
|
-
options: { auth: config.uxcAuth },
|
|
40
|
-
sink: "memory:",
|
|
41
|
-
ephemeral: false,
|
|
42
|
-
transportHint: "feishu_long_connection",
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
async readSubscriptionEvents(jobId, afterSeq) {
|
|
46
|
-
const response = await this.client.subscriptionEvents({
|
|
47
|
-
jobId,
|
|
48
|
-
afterSeq,
|
|
49
|
-
limit: 100,
|
|
50
|
-
waitMs: 10,
|
|
51
|
-
});
|
|
52
|
-
return {
|
|
53
|
-
events: response.events,
|
|
54
|
-
nextAfterSeq: response.next_after_seq,
|
|
55
|
-
status: response.status,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
15
|
async sendChatMessage(input) {
|
|
59
16
|
await this.client.call({
|
|
60
17
|
endpoint: input.endpoint ?? exports.FEISHU_OPENAPI_ENDPOINT,
|
|
@@ -91,176 +48,6 @@ class FeishuUxcClient {
|
|
|
91
48
|
}
|
|
92
49
|
}
|
|
93
50
|
exports.FeishuUxcClient = FeishuUxcClient;
|
|
94
|
-
class FeishuSourceRuntime {
|
|
95
|
-
store;
|
|
96
|
-
appendSourceEvent;
|
|
97
|
-
client;
|
|
98
|
-
interval = null;
|
|
99
|
-
inFlight = new Set();
|
|
100
|
-
errorCounts = new Map();
|
|
101
|
-
nextRetryAt = new Map();
|
|
102
|
-
constructor(store, appendSourceEvent, client) {
|
|
103
|
-
this.store = store;
|
|
104
|
-
this.appendSourceEvent = appendSourceEvent;
|
|
105
|
-
this.client = client ?? new FeishuUxcClient();
|
|
106
|
-
}
|
|
107
|
-
async ensureSource(source) {
|
|
108
|
-
if (source.sourceType !== "feishu_bot") {
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
const config = parseFeishuSourceConfig(source);
|
|
112
|
-
const checkpoint = parseFeishuCheckpoint(source.checkpoint);
|
|
113
|
-
const started = await this.client.ensureLongConnectionSubscription(config, checkpoint);
|
|
114
|
-
this.store.updateSourceRuntime(source.sourceId, {
|
|
115
|
-
status: "active",
|
|
116
|
-
checkpoint: JSON.stringify({
|
|
117
|
-
...checkpoint,
|
|
118
|
-
uxcJobId: started.job_id,
|
|
119
|
-
}),
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
async start() {
|
|
123
|
-
if (this.interval) {
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
this.interval = setInterval(() => {
|
|
127
|
-
void this.syncAll();
|
|
128
|
-
}, 2_000);
|
|
129
|
-
try {
|
|
130
|
-
await this.syncAll();
|
|
131
|
-
}
|
|
132
|
-
catch (error) {
|
|
133
|
-
console.warn("feishu_bot initial sync failed:", error);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
async stop() {
|
|
137
|
-
if (this.interval) {
|
|
138
|
-
clearInterval(this.interval);
|
|
139
|
-
this.interval = null;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
async pollSource(sourceId) {
|
|
143
|
-
return this.syncSource(sourceId);
|
|
144
|
-
}
|
|
145
|
-
status() {
|
|
146
|
-
return {
|
|
147
|
-
activeSourceIds: Array.from(this.inFlight.values()).sort(),
|
|
148
|
-
erroredSourceIds: Array.from(this.errorCounts.keys()).sort(),
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
async syncAll() {
|
|
152
|
-
const sources = this.store
|
|
153
|
-
.listSources()
|
|
154
|
-
.filter((source) => source.sourceType === "feishu_bot" && source.status !== "paused");
|
|
155
|
-
for (const source of sources) {
|
|
156
|
-
try {
|
|
157
|
-
await this.syncSource(source.sourceId);
|
|
158
|
-
}
|
|
159
|
-
catch (error) {
|
|
160
|
-
console.warn(`feishu_bot sync failed for ${source.sourceId}:`, error);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
async syncSource(sourceId) {
|
|
165
|
-
if (this.inFlight.has(sourceId)) {
|
|
166
|
-
return {
|
|
167
|
-
sourceId,
|
|
168
|
-
sourceType: "feishu_bot",
|
|
169
|
-
appended: 0,
|
|
170
|
-
deduped: 0,
|
|
171
|
-
eventsRead: 0,
|
|
172
|
-
note: "source sync already in flight",
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
this.inFlight.add(sourceId);
|
|
176
|
-
try {
|
|
177
|
-
const source = this.store.getSource(sourceId);
|
|
178
|
-
if (!source) {
|
|
179
|
-
throw new Error(`unknown source: ${sourceId}`);
|
|
180
|
-
}
|
|
181
|
-
const config = parseFeishuSourceConfig(source);
|
|
182
|
-
if (source.status === "error") {
|
|
183
|
-
const retryAt = this.nextRetryAt.get(sourceId) ?? 0;
|
|
184
|
-
if (Date.now() < retryAt) {
|
|
185
|
-
return {
|
|
186
|
-
sourceId,
|
|
187
|
-
sourceType: "feishu_bot",
|
|
188
|
-
appended: 0,
|
|
189
|
-
deduped: 0,
|
|
190
|
-
eventsRead: 0,
|
|
191
|
-
note: "error backoff not elapsed",
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
let checkpoint = parseFeishuCheckpoint(source.checkpoint);
|
|
196
|
-
const subscription = await this.client.ensureLongConnectionSubscription(config, checkpoint);
|
|
197
|
-
if (subscription.job_id !== checkpoint.uxcJobId) {
|
|
198
|
-
checkpoint = { ...checkpoint, uxcJobId: subscription.job_id };
|
|
199
|
-
}
|
|
200
|
-
const batch = await this.client.readSubscriptionEvents(checkpoint.uxcJobId, checkpoint.afterSeq ?? 0);
|
|
201
|
-
let appended = 0;
|
|
202
|
-
let deduped = 0;
|
|
203
|
-
for (const event of batch.events) {
|
|
204
|
-
if (event.event_kind !== "data") {
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
const normalized = normalizeFeishuBotEvent(source, config, event.data);
|
|
208
|
-
if (!normalized) {
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
const result = await this.appendSourceEvent(normalized);
|
|
212
|
-
appended += result.appended;
|
|
213
|
-
deduped += result.deduped;
|
|
214
|
-
}
|
|
215
|
-
this.store.updateSourceRuntime(sourceId, {
|
|
216
|
-
status: "active",
|
|
217
|
-
checkpoint: JSON.stringify({
|
|
218
|
-
...checkpoint,
|
|
219
|
-
uxcJobId: checkpoint.uxcJobId,
|
|
220
|
-
afterSeq: batch.nextAfterSeq,
|
|
221
|
-
lastEventAt: new Date().toISOString(),
|
|
222
|
-
lastError: null,
|
|
223
|
-
}),
|
|
224
|
-
});
|
|
225
|
-
this.errorCounts.delete(sourceId);
|
|
226
|
-
this.nextRetryAt.delete(sourceId);
|
|
227
|
-
return {
|
|
228
|
-
sourceId,
|
|
229
|
-
sourceType: "feishu_bot",
|
|
230
|
-
appended,
|
|
231
|
-
deduped,
|
|
232
|
-
eventsRead: batch.events.length,
|
|
233
|
-
note: `subscription status=${batch.status}`,
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
catch (error) {
|
|
237
|
-
const source = this.store.getSource(sourceId);
|
|
238
|
-
if (source) {
|
|
239
|
-
const checkpoint = parseFeishuCheckpoint(source.checkpoint);
|
|
240
|
-
const nextErrorCount = (this.errorCounts.get(sourceId) ?? 0) + 1;
|
|
241
|
-
this.errorCounts.set(sourceId, nextErrorCount);
|
|
242
|
-
this.nextRetryAt.set(sourceId, Date.now() + computeErrorBackoffMs(DEFAULT_SYNC_INTERVAL_SECS, nextErrorCount));
|
|
243
|
-
this.store.updateSourceRuntime(sourceId, {
|
|
244
|
-
status: "error",
|
|
245
|
-
checkpoint: JSON.stringify({
|
|
246
|
-
...checkpoint,
|
|
247
|
-
lastError: error instanceof Error ? error.message : String(error),
|
|
248
|
-
}),
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
throw error;
|
|
252
|
-
}
|
|
253
|
-
finally {
|
|
254
|
-
this.inFlight.delete(sourceId);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
exports.FeishuSourceRuntime = FeishuSourceRuntime;
|
|
259
|
-
function computeErrorBackoffMs(syncIntervalSecs, errorCount) {
|
|
260
|
-
const baseMs = Math.max(1, syncIntervalSecs) * 1000;
|
|
261
|
-
const multiplier = Math.min(2 ** Math.max(0, errorCount - 1), MAX_ERROR_BACKOFF_MULTIPLIER);
|
|
262
|
-
return baseMs * multiplier;
|
|
263
|
-
}
|
|
264
51
|
class FeishuDeliveryAdapter {
|
|
265
52
|
client;
|
|
266
53
|
constructor(client) {
|
|
@@ -376,17 +163,6 @@ function parseFeishuSourceConfig(source) {
|
|
|
376
163
|
chatIds: asStringArray(config.chatIds) ?? undefined,
|
|
377
164
|
};
|
|
378
165
|
}
|
|
379
|
-
function parseFeishuCheckpoint(checkpoint) {
|
|
380
|
-
if (!checkpoint) {
|
|
381
|
-
return {};
|
|
382
|
-
}
|
|
383
|
-
try {
|
|
384
|
-
return JSON.parse(checkpoint);
|
|
385
|
-
}
|
|
386
|
-
catch {
|
|
387
|
-
return {};
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
166
|
function parseDeliveryConfig(payload) {
|
|
391
167
|
return {
|
|
392
168
|
endpoint: asString(payload.endpoint) ?? undefined,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.GithubDeliveryAdapter = exports.
|
|
3
|
+
exports.GithubDeliveryAdapter = exports.GithubUxcClient = exports.DEFAULT_GITHUB_EVENT_TYPES = exports.GITHUB_ENDPOINT = void 0;
|
|
4
4
|
exports.normalizeGithubRepoEvent = normalizeGithubRepoEvent;
|
|
5
5
|
exports.parseGithubSourceConfig = parseGithubSourceConfig;
|
|
6
6
|
const uxc_daemon_client_1 = require("@holon-run/uxc-daemon-client");
|
|
@@ -11,64 +11,11 @@ exports.DEFAULT_GITHUB_EVENT_TYPES = [
|
|
|
11
11
|
"PullRequestEvent",
|
|
12
12
|
"PullRequestReviewCommentEvent",
|
|
13
13
|
];
|
|
14
|
-
const MAX_ERROR_BACKOFF_MULTIPLIER = 8;
|
|
15
14
|
class GithubUxcClient {
|
|
16
15
|
client;
|
|
17
16
|
constructor(client = new uxc_daemon_client_1.UxcDaemonClient({ env: process.env })) {
|
|
18
17
|
this.client = client;
|
|
19
18
|
}
|
|
20
|
-
async ensureRepoEventsSubscription(config, checkpoint) {
|
|
21
|
-
if (checkpoint.uxcJobId) {
|
|
22
|
-
try {
|
|
23
|
-
const status = await this.client.subscribeStatus(checkpoint.uxcJobId);
|
|
24
|
-
if (status.status === "running" || status.status === "reconnecting") {
|
|
25
|
-
return {
|
|
26
|
-
job_id: checkpoint.uxcJobId,
|
|
27
|
-
mode: "poll",
|
|
28
|
-
protocol: "openapi",
|
|
29
|
-
endpoint: exports.GITHUB_ENDPOINT,
|
|
30
|
-
sink: "memory:",
|
|
31
|
-
status: status.status,
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
// Recreate the job below.
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
const started = await this.client.subscribeStart({
|
|
40
|
-
endpoint: exports.GITHUB_ENDPOINT,
|
|
41
|
-
operationId: "get:/repos/{owner}/{repo}/events",
|
|
42
|
-
args: { owner: config.owner, repo: config.repo, per_page: config.perPage ?? 10 },
|
|
43
|
-
mode: "poll",
|
|
44
|
-
pollConfig: {
|
|
45
|
-
interval_secs: config.pollIntervalSecs ?? 30,
|
|
46
|
-
extract_items_pointer: "",
|
|
47
|
-
checkpoint_strategy: {
|
|
48
|
-
type: "item_key",
|
|
49
|
-
item_key_pointer: "/id",
|
|
50
|
-
seen_window: 1024,
|
|
51
|
-
},
|
|
52
|
-
},
|
|
53
|
-
options: { auth: config.uxcAuth },
|
|
54
|
-
sink: "memory:",
|
|
55
|
-
ephemeral: false,
|
|
56
|
-
});
|
|
57
|
-
return started;
|
|
58
|
-
}
|
|
59
|
-
async readSubscriptionEvents(jobId, afterSeq) {
|
|
60
|
-
const response = await this.client.subscriptionEvents({
|
|
61
|
-
jobId,
|
|
62
|
-
afterSeq,
|
|
63
|
-
limit: 100,
|
|
64
|
-
waitMs: 10,
|
|
65
|
-
});
|
|
66
|
-
return {
|
|
67
|
-
events: response.events,
|
|
68
|
-
nextAfterSeq: response.next_after_seq,
|
|
69
|
-
status: response.status,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
19
|
async createIssueComment(input) {
|
|
73
20
|
await this.client.call({
|
|
74
21
|
endpoint: exports.GITHUB_ENDPOINT,
|
|
@@ -98,177 +45,6 @@ class GithubUxcClient {
|
|
|
98
45
|
}
|
|
99
46
|
}
|
|
100
47
|
exports.GithubUxcClient = GithubUxcClient;
|
|
101
|
-
class GithubSourceRuntime {
|
|
102
|
-
store;
|
|
103
|
-
appendSourceEvent;
|
|
104
|
-
client;
|
|
105
|
-
interval = null;
|
|
106
|
-
inFlight = new Set();
|
|
107
|
-
errorCounts = new Map();
|
|
108
|
-
nextRetryAt = new Map();
|
|
109
|
-
constructor(store, appendSourceEvent, client) {
|
|
110
|
-
this.store = store;
|
|
111
|
-
this.appendSourceEvent = appendSourceEvent;
|
|
112
|
-
this.client = client ?? new GithubUxcClient();
|
|
113
|
-
}
|
|
114
|
-
async ensureSource(source) {
|
|
115
|
-
if (source.sourceType !== "github_repo") {
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
const config = parseGithubSourceConfig(source);
|
|
119
|
-
const checkpoint = parseGithubCheckpoint(source.checkpoint);
|
|
120
|
-
const started = await this.client.ensureRepoEventsSubscription(config, checkpoint);
|
|
121
|
-
this.store.updateSourceRuntime(source.sourceId, {
|
|
122
|
-
status: "active",
|
|
123
|
-
checkpoint: JSON.stringify({
|
|
124
|
-
...checkpoint,
|
|
125
|
-
uxcJobId: started.job_id,
|
|
126
|
-
}),
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
async start() {
|
|
130
|
-
if (this.interval) {
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
this.interval = setInterval(() => {
|
|
134
|
-
void this.syncAll();
|
|
135
|
-
}, 2_000);
|
|
136
|
-
try {
|
|
137
|
-
await this.syncAll();
|
|
138
|
-
}
|
|
139
|
-
catch (error) {
|
|
140
|
-
console.warn("github_repo initial sync failed:", error);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
async stop() {
|
|
144
|
-
if (this.interval) {
|
|
145
|
-
clearInterval(this.interval);
|
|
146
|
-
this.interval = null;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
async pollSource(sourceId) {
|
|
150
|
-
return this.syncSource(sourceId);
|
|
151
|
-
}
|
|
152
|
-
status() {
|
|
153
|
-
return {
|
|
154
|
-
activeSourceIds: Array.from(this.inFlight.values()).sort(),
|
|
155
|
-
erroredSourceIds: Array.from(this.errorCounts.keys()).sort(),
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
async syncAll() {
|
|
159
|
-
const sources = this.store
|
|
160
|
-
.listSources()
|
|
161
|
-
.filter((source) => source.sourceType === "github_repo" && source.status !== "paused");
|
|
162
|
-
for (const source of sources) {
|
|
163
|
-
try {
|
|
164
|
-
await this.syncSource(source.sourceId);
|
|
165
|
-
}
|
|
166
|
-
catch (error) {
|
|
167
|
-
console.warn(`github_repo sync failed for ${source.sourceId}:`, error);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
async syncSource(sourceId) {
|
|
172
|
-
if (this.inFlight.has(sourceId)) {
|
|
173
|
-
return {
|
|
174
|
-
sourceId,
|
|
175
|
-
sourceType: "github_repo",
|
|
176
|
-
appended: 0,
|
|
177
|
-
deduped: 0,
|
|
178
|
-
eventsRead: 0,
|
|
179
|
-
note: "source sync already in flight",
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
this.inFlight.add(sourceId);
|
|
183
|
-
try {
|
|
184
|
-
const source = this.store.getSource(sourceId);
|
|
185
|
-
if (!source) {
|
|
186
|
-
throw new Error(`unknown source: ${sourceId}`);
|
|
187
|
-
}
|
|
188
|
-
const config = parseGithubSourceConfig(source);
|
|
189
|
-
if (source.status === "error") {
|
|
190
|
-
const retryAt = this.nextRetryAt.get(sourceId) ?? 0;
|
|
191
|
-
if (Date.now() < retryAt) {
|
|
192
|
-
return {
|
|
193
|
-
sourceId,
|
|
194
|
-
sourceType: "github_repo",
|
|
195
|
-
appended: 0,
|
|
196
|
-
deduped: 0,
|
|
197
|
-
eventsRead: 0,
|
|
198
|
-
note: "error backoff not elapsed",
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
let checkpoint = parseGithubCheckpoint(source.checkpoint);
|
|
203
|
-
const subscription = await this.client.ensureRepoEventsSubscription(config, checkpoint);
|
|
204
|
-
if (subscription.job_id !== checkpoint.uxcJobId) {
|
|
205
|
-
checkpoint = { ...checkpoint, uxcJobId: subscription.job_id };
|
|
206
|
-
}
|
|
207
|
-
const batch = await this.client.readSubscriptionEvents(checkpoint.uxcJobId, checkpoint.afterSeq ?? 0);
|
|
208
|
-
let appended = 0;
|
|
209
|
-
let deduped = 0;
|
|
210
|
-
for (const event of batch.events) {
|
|
211
|
-
if (event.event_kind !== "data") {
|
|
212
|
-
continue;
|
|
213
|
-
}
|
|
214
|
-
const normalized = normalizeGithubRepoEvent(source, config, event.data);
|
|
215
|
-
if (!normalized) {
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
const result = await this.appendSourceEvent(normalized);
|
|
219
|
-
appended += result.appended;
|
|
220
|
-
deduped += result.deduped;
|
|
221
|
-
}
|
|
222
|
-
this.store.updateSourceRuntime(sourceId, {
|
|
223
|
-
status: "active",
|
|
224
|
-
checkpoint: JSON.stringify({
|
|
225
|
-
...checkpoint,
|
|
226
|
-
uxcJobId: checkpoint.uxcJobId,
|
|
227
|
-
afterSeq: batch.nextAfterSeq,
|
|
228
|
-
lastEventAt: new Date().toISOString(),
|
|
229
|
-
lastError: null,
|
|
230
|
-
}),
|
|
231
|
-
});
|
|
232
|
-
this.errorCounts.delete(sourceId);
|
|
233
|
-
this.nextRetryAt.delete(sourceId);
|
|
234
|
-
return {
|
|
235
|
-
sourceId,
|
|
236
|
-
sourceType: "github_repo",
|
|
237
|
-
appended,
|
|
238
|
-
deduped,
|
|
239
|
-
eventsRead: batch.events.length,
|
|
240
|
-
note: `subscription status=${batch.status}`,
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
catch (error) {
|
|
244
|
-
const source = this.store.getSource(sourceId);
|
|
245
|
-
if (source) {
|
|
246
|
-
const checkpoint = parseGithubCheckpoint(source.checkpoint);
|
|
247
|
-
const config = parseGithubSourceConfig(source);
|
|
248
|
-
const nextErrorCount = (this.errorCounts.get(sourceId) ?? 0) + 1;
|
|
249
|
-
this.errorCounts.set(sourceId, nextErrorCount);
|
|
250
|
-
this.nextRetryAt.set(sourceId, Date.now() + computeErrorBackoffMs(config.pollIntervalSecs ?? 30, nextErrorCount));
|
|
251
|
-
this.store.updateSourceRuntime(sourceId, {
|
|
252
|
-
status: "error",
|
|
253
|
-
checkpoint: JSON.stringify({
|
|
254
|
-
...checkpoint,
|
|
255
|
-
lastError: error instanceof Error ? error.message : String(error),
|
|
256
|
-
}),
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
throw error;
|
|
260
|
-
}
|
|
261
|
-
finally {
|
|
262
|
-
this.inFlight.delete(sourceId);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
exports.GithubSourceRuntime = GithubSourceRuntime;
|
|
267
|
-
function computeErrorBackoffMs(pollIntervalSecs, errorCount) {
|
|
268
|
-
const baseMs = Math.max(1, pollIntervalSecs) * 1000;
|
|
269
|
-
const multiplier = Math.min(2 ** Math.max(0, errorCount - 1), MAX_ERROR_BACKOFF_MULTIPLIER);
|
|
270
|
-
return baseMs * multiplier;
|
|
271
|
-
}
|
|
272
48
|
class GithubDeliveryAdapter {
|
|
273
49
|
client;
|
|
274
50
|
constructor(client) {
|
|
@@ -332,7 +108,7 @@ function normalizeGithubRepoEvent(source, config, raw) {
|
|
|
332
108
|
const url = asString(comment.html_url) ??
|
|
333
109
|
asString(issue.html_url) ??
|
|
334
110
|
asString(pullRequest.html_url) ??
|
|
335
|
-
asString(event
|
|
111
|
+
asString(event.url);
|
|
336
112
|
const deliveryHandle = buildGithubDeliveryHandle(config, eventType, number, comment);
|
|
337
113
|
return {
|
|
338
114
|
sourceId: source.sourceId,
|
|
@@ -416,17 +192,6 @@ function parseGithubSourceConfig(source) {
|
|
|
416
192
|
eventTypes: asStringArray(config.eventTypes) ?? exports.DEFAULT_GITHUB_EVENT_TYPES,
|
|
417
193
|
};
|
|
418
194
|
}
|
|
419
|
-
function parseGithubCheckpoint(checkpoint) {
|
|
420
|
-
if (!checkpoint) {
|
|
421
|
-
return {};
|
|
422
|
-
}
|
|
423
|
-
try {
|
|
424
|
-
return JSON.parse(checkpoint);
|
|
425
|
-
}
|
|
426
|
-
catch {
|
|
427
|
-
return {};
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
195
|
function extractLabels(raw) {
|
|
431
196
|
if (!Array.isArray(raw)) {
|
|
432
197
|
return [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@holon-run/agentinbox",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Local event subscription and delivery service for agents.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -37,11 +37,11 @@
|
|
|
37
37
|
"docs:deploy": "npm run docs:index && npm run docs:build && npx wrangler@4 deploy --config wrangler.jsonc",
|
|
38
38
|
"db:migrations:generate": "drizzle-kit generate --config drizzle.config.ts",
|
|
39
39
|
"prepack": "npm run build",
|
|
40
|
-
"test": "node --require ts-node/register --test test/*.test.ts"
|
|
40
|
+
"test": "node --require ts-node/register --test --test-force-exit test/*.test.ts"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@fastify/swagger": "^9.5.2",
|
|
44
|
-
"@holon-run/uxc-daemon-client": "^0.
|
|
44
|
+
"@holon-run/uxc-daemon-client": "^0.15.0",
|
|
45
45
|
"fastify": "^5.6.1",
|
|
46
46
|
"jexl": "^2.3.0",
|
|
47
47
|
"sql.js": "^1.13.0"
|