@agentshouse/kit 0.1.0-alpha.26 → 0.1.0-alpha.27

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/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 { googlePage } from "./google/source.js";
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 (context.instance.preset === "google") {
36
+ if (service.google !== undefined) {
34
37
  return googlePage(
35
38
  {
36
39
  directory: context.directory,
37
- instance: context.instance.id,
38
- client: context.googleOAuthClient,
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
- health: (await readHealth(options.directory, instance.id, service.service)) ?? {
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,72 @@
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) {
37
+ pending = false;
38
+ return;
39
+ }
40
+ const pause = new AbortController();
41
+ try {
42
+ await Promise.race([
43
+ new Promise((resolve) => {
44
+ release = resolve;
45
+ }),
46
+ delay(milliseconds, undefined, { signal: AbortSignal.any([signal, pause.signal]) }),
47
+ ]);
48
+ } catch (error) {
49
+ if (!signal.aborted) throw error;
50
+ } finally {
51
+ release = null;
52
+ pending = false;
53
+ pause.abort();
54
+ }
55
+ },
56
+ };
57
+ }
58
+
59
+ const turns = new Map();
60
+
61
+ export function exclusively(path, work) {
62
+ const turn = (turns.get(path) ?? Promise.resolve()).then(work);
63
+ const settled = turn.then(
64
+ () => undefined,
65
+ () => undefined,
66
+ );
67
+ turns.set(path, settled);
68
+ void settled.then(() => {
69
+ if (turns.get(path) === settled) turns.delete(path);
70
+ });
71
+ return turn;
72
+ }
package/src/worker.js CHANGED
@@ -20,7 +20,9 @@ import { localRequest, reconcileInbound } from "./inbound.js";
20
20
  import { localFiles } from "./local-files.js";
21
21
  import { answerText, answerYaml, editRefusal } from "./mcp.js";
22
22
  import { inheritedEnvironment } from "./command.js";
23
+ import { sourceEntries } from "./google/entries.js";
23
24
  import { watchKitWork } from "./kit-work-stream.js";
25
+ import { coalescedTask, residentWake } from "./tasks.js";
24
26
  import {
25
27
  applyWorkingCopies,
26
28
  deliverWorkingCopies,
@@ -955,62 +957,6 @@ async function reportedWorkingCopies(act, context, signal) {
955
957
  }
956
958
  }
957
959
 
958
- function coalescedTask(signal, fail, work) {
959
- let pending = false;
960
- let task = null;
961
- const trigger = () => {
962
- pending = true;
963
- if (task !== null) return;
964
- task = (async () => {
965
- while (pending && !signal.aborted) {
966
- pending = false;
967
- await work();
968
- }
969
- })()
970
- .catch((error) => {
971
- if (signal.aborted) return;
972
- fail(error);
973
- })
974
- .finally(() => {
975
- task = null;
976
- if (pending && !signal.aborted) trigger();
977
- });
978
- };
979
- return { trigger, idle: () => task === null, settled: () => task };
980
- }
981
-
982
- function residentWake() {
983
- let pending = false;
984
- let release = null;
985
- return {
986
- notify() {
987
- pending = true;
988
- release?.();
989
- },
990
- async wait(milliseconds, signal) {
991
- if (pending) {
992
- pending = false;
993
- return;
994
- }
995
- const pause = new AbortController();
996
- try {
997
- await Promise.race([
998
- new Promise((resolve) => {
999
- release = resolve;
1000
- }),
1001
- delay(milliseconds, undefined, { signal: AbortSignal.any([signal, pause.signal]) }),
1002
- ]);
1003
- } catch (error) {
1004
- if (!signal.aborted) throw error;
1005
- } finally {
1006
- release = null;
1007
- pending = false;
1008
- pause.abort();
1009
- }
1010
- },
1011
- };
1012
- }
1013
-
1014
960
  function residentUpdate(context, wake) {
1015
961
  let requested = existsSync(updateDrain(context.directory));
1016
962
  let ready = null;
@@ -1078,6 +1024,7 @@ export async function runResident(options, dependencies = {}) {
1078
1024
  const wake = residentWake();
1079
1025
  const update = residentUpdate(context, wake);
1080
1026
  context.update = update;
1027
+ let sources = null;
1081
1028
  const served = await serveResident(
1082
1029
  session,
1083
1030
  context.directory,
@@ -1087,6 +1034,8 @@ export async function runResident(options, dependencies = {}) {
1087
1034
  pushWorkingCopy({ directory: context.directory, session }, request, ownerCaller(session), signal).catch(
1088
1035
  (error) => ({ refused: true, text: formatCommandError(error) }),
1089
1036
  ),
1037
+ (source) =>
1038
+ sources === null ? Promise.reject(new KitRefusal("kit_resident_unavailable")) : sources.capture(source),
1090
1039
  );
1091
1040
  const serve = async (signal) => {
1092
1041
  context.busy = true;
@@ -1149,13 +1098,24 @@ export async function runResident(options, dependencies = {}) {
1149
1098
  const deliveries = coalescedTask(signal, unavailable, () =>
1150
1099
  reportedWorkingCopies(deliverWorkingCopies, context, signal),
1151
1100
  );
1101
+ sources = sourceEntries({
1102
+ session: context.session,
1103
+ directory: context.directory,
1104
+ dependencies,
1105
+ signal: AbortSignal.any([signal, watcher.signal]),
1106
+ now: session.now,
1107
+ draining: () => update.requested,
1108
+ unavailable: (error) => reported(`google sources unavailable: ${error?.message ?? error}`),
1109
+ });
1110
+ await sources.start();
1152
1111
  const pointer = () => {
1153
1112
  if (!update.requested) agentRefresh.trigger();
1154
1113
  };
1155
- const available = (subject) => {
1114
+ const available = (subject = null) => {
1156
1115
  if (update.requested) return;
1157
- if (subject !== "working_copy") agentRefresh.trigger();
1158
- if (subject !== "agents") copies.trigger();
1116
+ if (subject === null || subject === "agents") agentRefresh.trigger();
1117
+ if (subject === null || subject === "working_copy") copies.trigger();
1118
+ if (subject === null || subject === "sources") sources.reconcile();
1159
1119
  };
1160
1120
  context.onIdle = pointer;
1161
1121
  context.workingCopiesApplied = copies.settled;
@@ -1180,7 +1140,12 @@ export async function runResident(options, dependencies = {}) {
1180
1140
  if (sessions.length > 0) update.answer({ ready: false, sessions });
1181
1141
  else if (
1182
1142
  session.refused ||
1183
- (!served.serving() && logins.size === 0 && agentRefresh.idle() && copies.idle() && deliveries.idle())
1143
+ (!served.serving() &&
1144
+ logins.size === 0 &&
1145
+ agentRefresh.idle() &&
1146
+ copies.idle() &&
1147
+ deliveries.idle() &&
1148
+ sources.idle())
1184
1149
  ) {
1185
1150
  return;
1186
1151
  }
@@ -1194,6 +1159,7 @@ export async function runResident(options, dependencies = {}) {
1194
1159
  await agentRefresh.settled();
1195
1160
  await copies.settled();
1196
1161
  await deliveries.settled();
1162
+ await sources.settled();
1197
1163
  for (const running of logins.values()) running.controller.abort();
1198
1164
  await Promise.all([...logins.values()].map((running) => running.task));
1199
1165
  await context.agentRefresh;
@@ -23,6 +23,7 @@ import { readNote, writeNote, writeText } from "./state.js";
23
23
  import { bootstrapBundle, discoveryOf, door, withSession } from "./sync.js";
24
24
  import { isGeneratedProvenance, sidecarPath, structuredOrReplace } from "./sync-library.js";
25
25
  import { locate, removeReplicaFile, writeReplicaFile } from "./sync-state.js";
26
+ import { exclusively } from "./tasks.js";
26
27
 
27
28
  const COMMITTER_NAME = "House Kit";
28
29
  const COMMITTER_EMAIL = "house-kit@localhost";
@@ -366,21 +367,6 @@ async function heldCopies(directory) {
366
367
  return held.filter((copy) => copy !== null);
367
368
  }
368
369
 
369
- const turns = new Map();
370
-
371
- function exclusively(path, work) {
372
- const turn = (turns.get(path) ?? Promise.resolve()).then(work);
373
- const settled = turn.then(
374
- () => undefined,
375
- () => undefined,
376
- );
377
- turns.set(path, settled);
378
- void settled.then(() => {
379
- if (turns.get(path) === settled) turns.delete(path);
380
- });
381
- return turn;
382
- }
383
-
384
370
  function received(scope, read, parents) {
385
371
  const files = read.files.filter((file) => file.writable);
386
372
  return { message: `House ${housePath(scope)} at position ${read.position}`, parents, files, whole: true };
@@ -1,146 +0,0 @@
1
- import { ITEM_BODY_MAX_BYTES } from "../contract.js";
2
- import { KitRefusal } from "../refusal.js";
3
- import { googleJson } from "./gateway.js";
4
-
5
- function valueAt(value, path) {
6
- let current = value;
7
- for (const segment of path.split(".")) {
8
- if (current === null || current === undefined || typeof current !== "object") return undefined;
9
- current = current[segment];
10
- }
11
- return current;
12
- }
13
-
14
- function requiredText(value, declaration) {
15
- if (typeof value !== "string" || value.length === 0) {
16
- throw new KitRefusal("google_provider_response_invalid", declaration);
17
- }
18
- return value;
19
- }
20
-
21
- function optionalText(value, declaration) {
22
- if (value === undefined || value === null) return null;
23
- return requiredText(value, declaration);
24
- }
25
-
26
- function cursorOf(value) {
27
- if (value === null) return { pageToken: null, checkpoint: null };
28
- let cursor;
29
- try {
30
- cursor = JSON.parse(value);
31
- } catch {
32
- throw new KitRefusal("google_checkpoint_invalid", "declared service");
33
- }
34
- if (
35
- typeof cursor !== "object" ||
36
- cursor === null ||
37
- (cursor.pageToken !== null && typeof cursor.pageToken !== "string") ||
38
- (cursor.checkpoint !== null && typeof cursor.checkpoint !== "string")
39
- ) {
40
- throw new KitRefusal("google_checkpoint_invalid", "declared service");
41
- }
42
- return cursor;
43
- }
44
-
45
- function composedBody(item, paths) {
46
- const parts = paths.map((path) => valueAt(item, path));
47
- if (parts.some((part) => !["string", "number", "boolean"].includes(typeof part))) {
48
- throw new KitRefusal("google_provider_response_invalid", "declared body");
49
- }
50
- const body = parts.map((part) => `${part}`).join("\n\n");
51
- if (Buffer.byteLength(body, "utf8") > ITEM_BODY_MAX_BYTES) {
52
- throw new KitRefusal("google_provider_response_invalid", "declared body");
53
- }
54
- return body;
55
- }
56
-
57
- function normalizedItem(service, item) {
58
- const fields = service.declaration.fields;
59
- const observedAt = requiredText(valueAt(item, fields.observedAt), "declared observed time");
60
- if (Number.isNaN(Date.parse(observedAt))) {
61
- throw new KitRefusal("google_provider_response_invalid", "declared observed time");
62
- }
63
- const container = requiredText(valueAt(item, fields.container), "declared container");
64
- const nativeVersion =
65
- fields.nativeVersion === null
66
- ? null
67
- : optionalText(valueAt(item, fields.nativeVersion), "declared native version");
68
- const providerModifiedAt =
69
- fields.providerModifiedAt === null
70
- ? null
71
- : optionalText(
72
- valueAt(item, fields.providerModifiedAt),
73
- "declared provider modification time",
74
- );
75
- if (providerModifiedAt !== null && Number.isNaN(Date.parse(providerModifiedAt))) {
76
- throw new KitRefusal("google_provider_response_invalid", "declared provider modification time");
77
- }
78
- return {
79
- namespace: `google/${service.service}`,
80
- nativeObjectId: requiredText(valueAt(item, fields.nativeId), "declared native id"),
81
- nativeVersion,
82
- providerModifiedAt:
83
- providerModifiedAt === null ? null : new Date(providerModifiedAt).toISOString(),
84
- observedAt: new Date(observedAt).toISOString(),
85
- body: composedBody(item, fields.body),
86
- author:
87
- fields.author === null
88
- ? null
89
- : optionalText(valueAt(item, fields.author), "declared author"),
90
- url:
91
- fields.url === null ? null : optionalText(valueAt(item, fields.url), "declared url"),
92
- attachments: [],
93
- container: `google/${service.service}/${container}`,
94
- };
95
- }
96
-
97
- export async function declaredPage(options, dependencies = {}) {
98
- const declared = options.service.declaration;
99
- const cursor = cursorOf(options.cursor);
100
- const url = new URL(declared.path, `https://${declared.host}`);
101
- for (const [name, value] of Object.entries(declared.query)) url.searchParams.set(name, value);
102
- url.searchParams.set(declared.pageSize, `${options.limit}`);
103
- if (cursor.pageToken !== null) {
104
- url.searchParams.set(declared.pageToken.request, cursor.pageToken);
105
- }
106
- if (cursor.checkpoint !== null) {
107
- url.searchParams.set(declared.pageToken.checkpointRequest, cursor.checkpoint);
108
- }
109
- const answer = await googleJson(
110
- {
111
- directory: options.directory,
112
- instance: options.instance,
113
- client: options.client,
114
- requiredScopes: declared.scopes,
115
- url: url.toString(),
116
- signal: options.signal,
117
- },
118
- dependencies,
119
- );
120
- const entries = valueAt(answer, declared.items);
121
- if (!Array.isArray(entries) || entries.length > options.limit) {
122
- throw new KitRefusal("google_provider_response_invalid", "declared items");
123
- }
124
- const pageToken = valueAt(answer, declared.pageToken.response);
125
- let next;
126
- if (pageToken !== undefined && pageToken !== null) {
127
- next = {
128
- pageToken: requiredText(pageToken, "declared page token"),
129
- checkpoint: cursor.checkpoint,
130
- };
131
- } else {
132
- next = {
133
- pageToken: null,
134
- checkpoint: requiredText(
135
- valueAt(answer, declared.pageToken.checkpointResponse),
136
- "declared checkpoint",
137
- ),
138
- };
139
- }
140
- return {
141
- items: entries.map((entry) => normalizedItem(options.service, entry)),
142
- dropped: [],
143
- cursor: JSON.stringify(next),
144
- more: pageToken !== undefined && pageToken !== null,
145
- };
146
- }
package/src/google/id.js DELETED
@@ -1 +0,0 @@
1
- export const GOOGLE_DRIVE_ID = /^[A-Za-z0-9_-]{1,512}$/;
@@ -1 +0,0 @@
1
- export const GOOGLE_OAUTH_CLIENT = Object.freeze({"id":"911965338518-867gdvuddb7kdkjo3qvpv6gds369alb7.apps.googleusercontent.com","secret":"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"});