@agentshouse/kit 0.1.0-alpha.26 → 0.1.0-alpha.28
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 +154 -145
- package/bin/kit.js +48 -46
- package/bootstrap.json +3 -3
- package/package.json +1 -1
- package/src/configuration.js +9 -209
- package/src/connector-login.js +3 -84
- package/src/contract.js +8 -3
- package/src/delivery.js +4 -7
- package/src/google/calendar.js +39 -45
- package/src/google/drive.js +61 -186
- package/src/google/entries.js +346 -0
- package/src/google/gateway.js +3 -6
- package/src/google/gmail.js +33 -23
- package/src/google/oauth.js +106 -66
- package/src/google/source.js +21 -23
- package/src/house.js +43 -16
- package/src/kit-work-stream.js +49 -27
- package/src/setup.js +8 -14
- package/src/source.js +51 -26
- package/src/state.js +19 -1
- package/src/tasks.js +75 -0
- package/src/worker.js +126 -115
- package/src/working-copy.js +19 -24
- package/src/google/declared.js +0 -146
- package/src/google/id.js +0 -1
- package/src/instance/google.js +0 -1
package/src/kit-work-stream.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { setTimeout as delay } from "node:timers/promises";
|
|
2
2
|
import WebSocket from "ws";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
KIT_CONTRACT_VERSION,
|
|
5
|
+
KIT_STREAM_RECONNECT_MAX_MS,
|
|
6
|
+
KIT_STREAM_RECONNECT_MS,
|
|
7
|
+
KIT_STREAM_REJOIN_MS,
|
|
8
|
+
} from "./contract.js";
|
|
4
9
|
import { heldControl } from "./house.js";
|
|
5
10
|
import { KitRefusal } from "./refusal.js";
|
|
6
11
|
|
|
7
12
|
const PROTOCOL = "house.kit.stream.1";
|
|
8
|
-
const
|
|
13
|
+
const WORK_SUBJECTS = ["runs", "commands", "agents", "working_copy", "sources"];
|
|
14
|
+
const FINAL_REFUSALS = ["kit_upgrade_required", "kit_stream_replaced"];
|
|
9
15
|
|
|
10
16
|
function streamAddress(house) {
|
|
11
17
|
const url = new URL("/kit/stream", house);
|
|
@@ -15,72 +21,88 @@ function streamAddress(house) {
|
|
|
15
21
|
return url;
|
|
16
22
|
}
|
|
17
23
|
|
|
18
|
-
|
|
24
|
+
function frameOf(data) {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(data.toString());
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function held(session, signal, heard, dependencies) {
|
|
19
33
|
const authorization = await heldControl(session, signal);
|
|
20
34
|
return new Promise((resolve, reject) => {
|
|
21
35
|
const Socket = dependencies.workSocket ?? WebSocket;
|
|
22
36
|
const socket = new Socket(streamAddress(session.house), PROTOCOL, {
|
|
23
37
|
headers: { authorization, "x-house-kit-version": KIT_CONTRACT_VERSION },
|
|
24
38
|
handshakeTimeout: 10_000,
|
|
25
|
-
maxPayload: 4096,
|
|
26
39
|
perMessageDeflate: false,
|
|
27
40
|
});
|
|
28
41
|
let finished = false;
|
|
29
|
-
const finish = (
|
|
42
|
+
const finish = (reason = null, refusal = null) => {
|
|
30
43
|
if (finished) return;
|
|
31
44
|
finished = true;
|
|
32
45
|
signal.removeEventListener("abort", stop);
|
|
33
|
-
if (
|
|
34
|
-
else reject(
|
|
46
|
+
if (refusal === null) resolve(reason);
|
|
47
|
+
else reject(refusal);
|
|
35
48
|
};
|
|
36
49
|
const stop = () => {
|
|
37
50
|
socket.terminate();
|
|
38
51
|
finish();
|
|
39
52
|
};
|
|
40
53
|
signal.addEventListener("abort", stop, { once: true });
|
|
41
|
-
socket.on("open", () => available(null));
|
|
54
|
+
socket.on("open", () => heard.available(null));
|
|
42
55
|
socket.on("message", (data) => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
if (frame?.type === "work_available" && WATCHED_SUBJECTS.includes(frame.subject)) {
|
|
50
|
-
available(frame.subject);
|
|
56
|
+
const frame = frameOf(data);
|
|
57
|
+
if (frame?.type === "work_available" && WORK_SUBJECTS.includes(frame.subject)) {
|
|
58
|
+
heard.available(frame.subject);
|
|
59
|
+
} else if (frame?.type === "entries") {
|
|
60
|
+
heard.changed();
|
|
51
61
|
}
|
|
52
62
|
});
|
|
53
63
|
socket.on("unexpected-response", (_request, response) => {
|
|
54
64
|
response.resume();
|
|
55
65
|
if (response.statusCode === 409) session.dependencies.contractRefused?.();
|
|
56
|
-
|
|
57
|
-
response.statusCode === 409 ? new KitRefusal("kit_upgrade_required") : null;
|
|
66
|
+
finish(null, response.statusCode === 409 ? new KitRefusal("kit_upgrade_required") : null);
|
|
58
67
|
socket.terminate();
|
|
59
|
-
finish(refusal);
|
|
60
68
|
});
|
|
61
69
|
socket.on("error", () => {
|
|
62
70
|
socket.terminate();
|
|
63
71
|
finish();
|
|
64
72
|
});
|
|
65
|
-
socket.on("close", () =>
|
|
73
|
+
socket.on("close", (_code, reason) => {
|
|
74
|
+
const closed = reason?.toString() ?? "";
|
|
75
|
+
finish(closed, closed === "replaced" ? new KitRefusal("kit_stream_replaced") : null);
|
|
76
|
+
});
|
|
66
77
|
if (signal.aborted) stop();
|
|
67
78
|
});
|
|
68
79
|
}
|
|
69
80
|
|
|
70
|
-
export async function watchKitWork(session, signal,
|
|
71
|
-
|
|
81
|
+
export async function watchKitWork(session, signal, heard, dependencies = {}) {
|
|
82
|
+
const random = dependencies.random ?? Math.random;
|
|
83
|
+
const sleep = dependencies.streamSleep ?? ((milliseconds) => delay(milliseconds, undefined, { signal }));
|
|
84
|
+
let failures = 0;
|
|
72
85
|
while (!signal.aborted) {
|
|
73
86
|
const began = Date.now();
|
|
87
|
+
let reason = null;
|
|
74
88
|
try {
|
|
75
|
-
await
|
|
89
|
+
reason = await held(session, signal, heard, dependencies);
|
|
76
90
|
} catch (error) {
|
|
77
|
-
if (error instanceof KitRefusal && error.code
|
|
78
|
-
if (signal.aborted) return;
|
|
91
|
+
if (error instanceof KitRefusal && FINAL_REFUSALS.includes(error.code)) throw error;
|
|
79
92
|
}
|
|
80
93
|
if (signal.aborted) return;
|
|
81
|
-
|
|
94
|
+
let wait;
|
|
95
|
+
if (reason === "authority_changed") {
|
|
96
|
+
failures = 0;
|
|
97
|
+
wait = random() * KIT_STREAM_REJOIN_MS;
|
|
98
|
+
} else {
|
|
99
|
+
if (Date.now() - began >= KIT_STREAM_RECONNECT_MAX_MS) failures = 0;
|
|
100
|
+
const ceiling = Math.min(KIT_STREAM_RECONNECT_MAX_MS, KIT_STREAM_RECONNECT_MS * 2 ** failures);
|
|
101
|
+
failures += 1;
|
|
102
|
+
wait = ceiling / 2 + (random() * ceiling) / 2;
|
|
103
|
+
}
|
|
82
104
|
try {
|
|
83
|
-
await
|
|
105
|
+
await sleep(wait);
|
|
84
106
|
} catch (error) {
|
|
85
107
|
if (!signal.aborted) throw error;
|
|
86
108
|
}
|
package/src/setup.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
updateDeclaredAgent,
|
|
7
7
|
} from "./agents.js";
|
|
8
8
|
import { readConfiguration } from "./configuration.js";
|
|
9
|
-
import {
|
|
9
|
+
import { customAuthorizationStatus } from "./custom/authorization.js";
|
|
10
10
|
import { GUIDED_LOGIN_TIMEOUT_MS } from "./contract.js";
|
|
11
11
|
import { launchBase, prepareLaunchBase } from "./directory.js";
|
|
12
12
|
import {
|
|
@@ -45,24 +45,14 @@ function agentModule(context, kind) {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
function connectorModule(context, instance) {
|
|
48
|
-
const gateway = connectorLogin(instance, context.dependencies);
|
|
49
48
|
return {
|
|
50
49
|
category: "connector",
|
|
51
50
|
name: instance.id,
|
|
52
51
|
login: { integration: instance.preset, instance: instance.id, service: instance.id },
|
|
53
52
|
install: null,
|
|
54
53
|
signedIn: () =>
|
|
55
|
-
|
|
56
|
-
guided:
|
|
57
|
-
gateway.guided === undefined
|
|
58
|
-
? null
|
|
59
|
-
: ({ present, signal }) =>
|
|
60
|
-
gateway.guided(instance, {
|
|
61
|
-
directory: context.directory,
|
|
62
|
-
dependencies: context.dependencies,
|
|
63
|
-
present,
|
|
64
|
-
signal,
|
|
65
|
-
}),
|
|
54
|
+
customAuthorizationStatus(instance, { directory: context.directory, signal: context.signal }),
|
|
55
|
+
guided: null,
|
|
66
56
|
};
|
|
67
57
|
}
|
|
68
58
|
|
|
@@ -144,8 +134,12 @@ async function setUpModule(context, module, install = true) {
|
|
|
144
134
|
return summary("ready");
|
|
145
135
|
}
|
|
146
136
|
|
|
137
|
+
export function signInModule(context, module) {
|
|
138
|
+
return setUpModule(context, module, false);
|
|
139
|
+
}
|
|
140
|
+
|
|
147
141
|
export function signInWantedAgent(context, kind) {
|
|
148
|
-
return
|
|
142
|
+
return signInModule(context, agentModule(context, kind));
|
|
149
143
|
}
|
|
150
144
|
|
|
151
145
|
export async function runSetup(context, request) {
|
package/src/source.js
CHANGED
|
@@ -13,7 +13,8 @@ import {
|
|
|
13
13
|
stallOf,
|
|
14
14
|
} from "./delivery.js";
|
|
15
15
|
import { captureBatch, maintainControl, openSession } from "./house.js";
|
|
16
|
-
import {
|
|
16
|
+
import { GOOGLE_SOURCES } from "./google/oauth.js";
|
|
17
|
+
import { googlePage, recordAccountEmail } from "./google/source.js";
|
|
17
18
|
import { houseAnswered, houseAnsweredFailure, houseFailed, KitRefusal } from "./refusal.js";
|
|
18
19
|
import { recordAcceptedOriginals, sameReference, saveSourceOriginals } from "./source-originals.js";
|
|
19
20
|
import {
|
|
@@ -29,14 +30,15 @@ import {
|
|
|
29
30
|
writeCheckpoint,
|
|
30
31
|
} from "./state.js";
|
|
31
32
|
|
|
33
|
+
const GOOGLE = Object.freeze({ id: GOOGLE_SOURCES, preset: "google" });
|
|
34
|
+
|
|
32
35
|
async function acquire(context, service, checkpoint, limit) {
|
|
33
|
-
if (
|
|
36
|
+
if (service.google !== undefined) {
|
|
34
37
|
return googlePage(
|
|
35
38
|
{
|
|
36
39
|
directory: context.directory,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
service,
|
|
40
|
+
account: service.google.account,
|
|
41
|
+
service: service.google.service,
|
|
40
42
|
cursor: checkpoint.cursor,
|
|
41
43
|
limit,
|
|
42
44
|
backfillFloor:
|
|
@@ -169,6 +171,7 @@ async function deliverService(context, service) {
|
|
|
169
171
|
context.instance.preset === "custom"
|
|
170
172
|
? { ...context, environment: await resolveEnvironment(service.connector) }
|
|
171
173
|
: context;
|
|
174
|
+
if (service.google !== undefined) await recordAccountEmail(context, service.google);
|
|
172
175
|
await runDeliveryLoop(resolved, service, progress);
|
|
173
176
|
} catch (refusal) {
|
|
174
177
|
if (!(refusal instanceof KitRefusal) || refusal.code === "kit_command_cancelled") throw refusal;
|
|
@@ -197,12 +200,31 @@ async function deliverService(context, service) {
|
|
|
197
200
|
};
|
|
198
201
|
}
|
|
199
202
|
|
|
203
|
+
function initialBackfill(initial) {
|
|
204
|
+
if (initial.new === true) return { kind: "days", bound: 0 };
|
|
205
|
+
if (initial.days !== undefined) return { kind: "days", bound: initial.days };
|
|
206
|
+
if (initial.items !== undefined) return { kind: "count", bound: initial.items };
|
|
207
|
+
return { kind: "all" };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export async function captureEntry(context, entry) {
|
|
211
|
+
const delivered = await deliverService(
|
|
212
|
+
{ ...context, instance: GOOGLE },
|
|
213
|
+
{
|
|
214
|
+
service: entry.source,
|
|
215
|
+
sourceRef: entry.source,
|
|
216
|
+
rooms: entry.rooms,
|
|
217
|
+
backfill: initialBackfill(entry.initial),
|
|
218
|
+
google: { account: entry.account, service: entry.service },
|
|
219
|
+
},
|
|
220
|
+
);
|
|
221
|
+
return { ...delivered, service: entry.service };
|
|
222
|
+
}
|
|
223
|
+
|
|
200
224
|
export async function captureSource(options, dependencies = {}) {
|
|
201
225
|
const configuration = await readConfiguration(options.configuration);
|
|
202
226
|
const instance = sourceInstance(configuration, options.instance);
|
|
203
227
|
const services = declaredServices(instance, options.service ?? null);
|
|
204
|
-
const googleOAuthClient =
|
|
205
|
-
instance.preset === "google" ? dependencies.googleOAuthClient() : null;
|
|
206
228
|
const session = await openSession(options, dependencies);
|
|
207
229
|
return maintainControl(
|
|
208
230
|
session,
|
|
@@ -214,7 +236,6 @@ export async function captureSource(options, dependencies = {}) {
|
|
|
214
236
|
signal,
|
|
215
237
|
now: session.now,
|
|
216
238
|
dependencies,
|
|
217
|
-
googleOAuthClient,
|
|
218
239
|
};
|
|
219
240
|
const delivered = [];
|
|
220
241
|
for (const service of services) delivered.push(await deliverService(context, service));
|
|
@@ -224,6 +245,27 @@ export async function captureSource(options, dependencies = {}) {
|
|
|
224
245
|
);
|
|
225
246
|
}
|
|
226
247
|
|
|
248
|
+
export async function localServiceState(directory, instance, service) {
|
|
249
|
+
return {
|
|
250
|
+
health: (await readHealth(directory, instance, service)) ?? {
|
|
251
|
+
state: "pending",
|
|
252
|
+
category: null,
|
|
253
|
+
observed_at: null,
|
|
254
|
+
},
|
|
255
|
+
checkpoint: await readCheckpoint(directory, instance, service),
|
|
256
|
+
gaps: await readCoverageGaps(directory, instance, service),
|
|
257
|
+
dropped: await readDroppedItems(directory, instance, service),
|
|
258
|
+
originals: (await readSourceOriginals(directory, instance, service)).map((reference) => ({
|
|
259
|
+
room: reference.room,
|
|
260
|
+
record: reference.record,
|
|
261
|
+
position: reference.position,
|
|
262
|
+
native_object_id: reference.native_object_id,
|
|
263
|
+
save: reference.save,
|
|
264
|
+
error: reference.error,
|
|
265
|
+
})),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
227
269
|
export async function sourceStatus(options) {
|
|
228
270
|
const configuration = await readConfiguration(options.configuration);
|
|
229
271
|
const instance = sourceInstance(configuration, options.instance);
|
|
@@ -234,24 +276,7 @@ export async function sourceStatus(options) {
|
|
|
234
276
|
service: service.service,
|
|
235
277
|
source_ref: service.sourceRef,
|
|
236
278
|
rooms: service.rooms,
|
|
237
|
-
|
|
238
|
-
state: "pending",
|
|
239
|
-
category: null,
|
|
240
|
-
observed_at: null,
|
|
241
|
-
},
|
|
242
|
-
checkpoint: await readCheckpoint(options.directory, instance.id, service.service),
|
|
243
|
-
gaps: await readCoverageGaps(options.directory, instance.id, service.service),
|
|
244
|
-
dropped: await readDroppedItems(options.directory, instance.id, service.service),
|
|
245
|
-
originals: (await readSourceOriginals(options.directory, instance.id, service.service)).map(
|
|
246
|
-
(reference) => ({
|
|
247
|
-
room: reference.room,
|
|
248
|
-
record: reference.record,
|
|
249
|
-
position: reference.position,
|
|
250
|
-
native_object_id: reference.native_object_id,
|
|
251
|
-
save: reference.save,
|
|
252
|
-
error: reference.error,
|
|
253
|
-
}),
|
|
254
|
-
),
|
|
279
|
+
...(await localServiceState(options.directory, instance.id, service.service)),
|
|
255
280
|
});
|
|
256
281
|
}
|
|
257
282
|
return { instance: instance.id, preset: instance.preset, services: reported };
|
package/src/state.js
CHANGED
|
@@ -9,10 +9,28 @@ function segment(name) {
|
|
|
9
9
|
return name;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
function notePath(directory, instance, service, kind) {
|
|
12
|
+
export function notePath(directory, instance, service, kind) {
|
|
13
13
|
return join(directory, "state", segment(instance), kind, `${segment(service)}.json`);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
export async function noteNames(directory, instance, kind) {
|
|
17
|
+
let present;
|
|
18
|
+
try {
|
|
19
|
+
present = await readdir(join(directory, "state", segment(instance), kind));
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (error.code === "ENOENT") return [];
|
|
22
|
+
throw new KitRefusal("kit_state_unreadable", kind);
|
|
23
|
+
}
|
|
24
|
+
return present
|
|
25
|
+
.filter((name) => name.endsWith(".json"))
|
|
26
|
+
.map((name) => name.slice(0, -".json".length))
|
|
27
|
+
.sort();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function removeNotes(directory, instance, service, kinds) {
|
|
31
|
+
for (const kind of kinds) await rm(notePath(directory, instance, service, kind), { force: true });
|
|
32
|
+
}
|
|
33
|
+
|
|
16
34
|
function repositoryPath(directory, key) {
|
|
17
35
|
if (!/^[a-f0-9]{64}$/.test(key)) throw new KitRefusal("kit_state_name_invalid", key);
|
|
18
36
|
return join(directory, "state", "repositories", `${key}.json`);
|
package/src/tasks.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
+
|
|
3
|
+
export function coalescedTask(signal, fail, work) {
|
|
4
|
+
let pending = false;
|
|
5
|
+
let task = null;
|
|
6
|
+
const trigger = () => {
|
|
7
|
+
pending = true;
|
|
8
|
+
if (task !== null) return;
|
|
9
|
+
task = (async () => {
|
|
10
|
+
while (pending && !signal.aborted) {
|
|
11
|
+
pending = false;
|
|
12
|
+
await work();
|
|
13
|
+
}
|
|
14
|
+
})()
|
|
15
|
+
.catch((error) => {
|
|
16
|
+
if (signal.aborted) return;
|
|
17
|
+
fail(error);
|
|
18
|
+
})
|
|
19
|
+
.finally(() => {
|
|
20
|
+
task = null;
|
|
21
|
+
if (pending && !signal.aborted) trigger();
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
return { trigger, idle: () => task === null, settled: () => task };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function residentWake() {
|
|
28
|
+
let pending = false;
|
|
29
|
+
let release = null;
|
|
30
|
+
return {
|
|
31
|
+
notify() {
|
|
32
|
+
pending = true;
|
|
33
|
+
release?.();
|
|
34
|
+
},
|
|
35
|
+
async wait(milliseconds, signal) {
|
|
36
|
+
if (!pending && !signal.aborted) {
|
|
37
|
+
const pause = new AbortController();
|
|
38
|
+
const paused = AbortSignal.any([signal, pause.signal]);
|
|
39
|
+
try {
|
|
40
|
+
await Promise.race([
|
|
41
|
+
new Promise((resolve) => {
|
|
42
|
+
release = resolve;
|
|
43
|
+
}),
|
|
44
|
+
Number.isFinite(milliseconds)
|
|
45
|
+
? delay(milliseconds, undefined, { signal: paused })
|
|
46
|
+
: new Promise((resolve) => paused.addEventListener("abort", resolve, { once: true })),
|
|
47
|
+
]);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (!signal.aborted) throw error;
|
|
50
|
+
} finally {
|
|
51
|
+
release = null;
|
|
52
|
+
pause.abort();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const woken = pending;
|
|
56
|
+
pending = false;
|
|
57
|
+
return woken;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const turns = new Map();
|
|
63
|
+
|
|
64
|
+
export function exclusively(path, work) {
|
|
65
|
+
const turn = (turns.get(path) ?? Promise.resolve()).then(work);
|
|
66
|
+
const settled = turn.then(
|
|
67
|
+
() => undefined,
|
|
68
|
+
() => undefined,
|
|
69
|
+
);
|
|
70
|
+
turns.set(path, settled);
|
|
71
|
+
void settled.then(() => {
|
|
72
|
+
if (turns.get(path) === settled) turns.delete(path);
|
|
73
|
+
});
|
|
74
|
+
return turn;
|
|
75
|
+
}
|