@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/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,
|
|
@@ -38,10 +40,8 @@ import {
|
|
|
38
40
|
MCP_REQUEST_BODY_MAX_BYTES,
|
|
39
41
|
PROVIDER_CONTINUATION_UNAVAILABLE,
|
|
40
42
|
WORKER_COMMAND_BATCH_MAX,
|
|
41
|
-
WORKER_COMMAND_POLL_MS,
|
|
42
43
|
WORKER_EXIT_GRACE_MS,
|
|
43
44
|
WORKER_EXIT_POLL_MS,
|
|
44
|
-
WORKER_HEARTBEAT_MS,
|
|
45
45
|
WORKER_INHERITED_ENVIRONMENT,
|
|
46
46
|
WORKER_SUPERVISION_POLL_MS,
|
|
47
47
|
WORKING_COPY_INDEX_METHOD,
|
|
@@ -74,7 +74,6 @@ import {
|
|
|
74
74
|
reportInteractionOutcome,
|
|
75
75
|
toolMutations,
|
|
76
76
|
reportRunOutcome,
|
|
77
|
-
reportSessionHeartbeat,
|
|
78
77
|
releaseControl,
|
|
79
78
|
reportSessions,
|
|
80
79
|
serveResident,
|
|
@@ -99,6 +98,7 @@ import {
|
|
|
99
98
|
|
|
100
99
|
const LAUNCHER_MAIN = fileURLToPath(new URL("launcher-main.js", import.meta.url));
|
|
101
100
|
const RESIDENT_INTERVAL_MS = 30_000;
|
|
101
|
+
const PASS_REACHES = ["commands", "runs"];
|
|
102
102
|
|
|
103
103
|
async function dispatchSeal(context, bound) {
|
|
104
104
|
try {
|
|
@@ -132,9 +132,7 @@ async function pendingCommands(context, kinds = ["stop"]) {
|
|
|
132
132
|
try {
|
|
133
133
|
return await acquireCommands(context.session, WORKER_COMMAND_BATCH_MAX, kinds);
|
|
134
134
|
} catch (refusal) {
|
|
135
|
-
if (houseFailed(refusal) || context.session.refused === true)
|
|
136
|
-
return { stops: [], answers: [], interrupts: [] };
|
|
137
|
-
}
|
|
135
|
+
if (houseFailed(refusal) || context.session.refused === true) return null;
|
|
138
136
|
throw refusal;
|
|
139
137
|
}
|
|
140
138
|
}
|
|
@@ -155,14 +153,6 @@ async function settleInterrupt(context, interrupt, outcome) {
|
|
|
155
153
|
}
|
|
156
154
|
}
|
|
157
155
|
|
|
158
|
-
async function reportHeartbeat(context, bound) {
|
|
159
|
-
try {
|
|
160
|
-
await reportSessionHeartbeat(context.session, bound.session);
|
|
161
|
-
} catch (refusal) {
|
|
162
|
-
if (!(refusal instanceof KitRefusal)) throw refusal;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
156
|
function retryFiles(context) {
|
|
167
157
|
if (context.retrying !== undefined) return;
|
|
168
158
|
context.retrying = retryConversationFiles(context)
|
|
@@ -185,40 +175,32 @@ async function settleFiles(context) {
|
|
|
185
175
|
}
|
|
186
176
|
|
|
187
177
|
async function supervise(context, bound, bridge) {
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
await Promise.race([terminal, delay(WORKER_SUPERVISION_POLL_MS)]);
|
|
197
|
-
if (!live) return null;
|
|
178
|
+
const departed = new AbortController();
|
|
179
|
+
bridge.departed.then(() => departed.abort());
|
|
180
|
+
const waiting = AbortSignal.any([context.signal, departed.signal]);
|
|
181
|
+
let owed = false;
|
|
182
|
+
for (;;) {
|
|
183
|
+
const commanded = (await context.commands.wait(WORKER_SUPERVISION_POLL_MS, waiting)) || owed;
|
|
184
|
+
if (departed.signal.aborted) return null;
|
|
185
|
+
if (context.signal.aborted) throw new KitRefusal("kit_command_cancelled");
|
|
198
186
|
await context.update?.announce();
|
|
199
|
-
if (
|
|
200
|
-
commanded = Date.now() + WORKER_COMMAND_POLL_MS;
|
|
187
|
+
if (commanded) {
|
|
201
188
|
const polled = await pendingCommands(context, ["stop", "interrupt", "answer"]);
|
|
202
|
-
|
|
189
|
+
owed = polled === null && context.session.refused !== true;
|
|
190
|
+
for (const answer of polled?.answers ?? []) {
|
|
203
191
|
if (answer.session === bound.session) bridge.answered(answer);
|
|
204
192
|
}
|
|
205
|
-
for (const interrupt of polled
|
|
193
|
+
for (const interrupt of polled?.interrupts ?? []) {
|
|
206
194
|
if (interrupt.session !== bound.session || interrupt.run !== bound.run) continue;
|
|
207
195
|
if (!bridge.interrupt(interrupt)) {
|
|
208
196
|
await settleInterrupt(context, interrupt.interrupt, "nothing_to_interrupt");
|
|
209
197
|
}
|
|
210
198
|
}
|
|
211
|
-
const stop = polled
|
|
199
|
+
const stop = polled?.stops.find((command) => command.run === bound.run);
|
|
212
200
|
if (stop !== undefined) return stop;
|
|
213
201
|
}
|
|
214
|
-
if (Date.now() >= pulsed) {
|
|
215
|
-
pulsed = Date.now() + WORKER_HEARTBEAT_MS;
|
|
216
|
-
retryFiles(context);
|
|
217
|
-
await reportHeartbeat(context, bound);
|
|
218
|
-
}
|
|
219
202
|
if (!(await sessionPresent(context.runtime, bound.tmux))) return null;
|
|
220
203
|
}
|
|
221
|
-
return null;
|
|
222
204
|
}
|
|
223
205
|
|
|
224
206
|
async function grantExitGrace(runtime, handle) {
|
|
@@ -430,6 +412,7 @@ async function superviseBound(context, bound, opened, start) {
|
|
|
430
412
|
} finally {
|
|
431
413
|
try {
|
|
432
414
|
try {
|
|
415
|
+
retryFiles(context);
|
|
433
416
|
await settleFiles(context);
|
|
434
417
|
for (const interaction of bridge.unsettledInteractions()) {
|
|
435
418
|
await settleInteraction(context, interaction, "expired");
|
|
@@ -725,7 +708,7 @@ async function reconcile(context) {
|
|
|
725
708
|
}
|
|
726
709
|
const bindings = await readBindings(context.directory);
|
|
727
710
|
if (bindings.length === 0) return { run: null, stopped, unresolved };
|
|
728
|
-
const commands = (await pendingCommands(context))
|
|
711
|
+
const commands = (await pendingCommands(context))?.stops ?? [];
|
|
729
712
|
const carried = [];
|
|
730
713
|
for (const binding of bindings) {
|
|
731
714
|
if (binding.reported !== null) {
|
|
@@ -809,7 +792,7 @@ function unlessDraining(context, act) {
|
|
|
809
792
|
return context.update?.requested ? null : act();
|
|
810
793
|
}
|
|
811
794
|
|
|
812
|
-
async function workerPass(context, dependencies, baseConfiguration) {
|
|
795
|
+
async function workerPass(context, dependencies, baseConfiguration, reach) {
|
|
813
796
|
const { session, signal } = context;
|
|
814
797
|
const reconciled = await reconcile(context);
|
|
815
798
|
if (reconciled.run !== null || reconciled.unresolved.length > 0 || context.update?.requested) {
|
|
@@ -840,6 +823,7 @@ async function workerPass(context, dependencies, baseConfiguration) {
|
|
|
840
823
|
}
|
|
841
824
|
const resumption = await unlessDraining(context, () => acquireOneResumption(session));
|
|
842
825
|
if (resumption !== null) return { ...reconciled, run: await superviseResumption(context, resumption) };
|
|
826
|
+
if (reach !== "runs") return reconciled;
|
|
843
827
|
const assignment = await unlessDraining(context, () => acquireOneRun(session));
|
|
844
828
|
if (assignment === null) return reconciled;
|
|
845
829
|
return { ...reconciled, run: await superviseAssignment(context, assignment) };
|
|
@@ -863,6 +847,7 @@ async function workerContext(options, dependencies, resident = false) {
|
|
|
863
847
|
directory: options.directory,
|
|
864
848
|
home: dependencies.home ?? homedir(),
|
|
865
849
|
files: { originals: [], failures: [] },
|
|
850
|
+
commands: residentWake(),
|
|
866
851
|
epochReport: null,
|
|
867
852
|
agentRefresh: Promise.resolve(),
|
|
868
853
|
configurationPath: options.configuration,
|
|
@@ -907,7 +892,7 @@ function queueAgentRefresh(context, dependencies, signal, configuration, pointer
|
|
|
907
892
|
return refreshed;
|
|
908
893
|
}
|
|
909
894
|
|
|
910
|
-
async function controlledPass(context, dependencies, configuration, signal) {
|
|
895
|
+
async function controlledPass(context, dependencies, configuration, signal, reach = "runs") {
|
|
911
896
|
context.signal = signal;
|
|
912
897
|
context.session.signal = signal;
|
|
913
898
|
await reconcileSkillSet({
|
|
@@ -917,7 +902,7 @@ async function controlledPass(context, dependencies, configuration, signal) {
|
|
|
917
902
|
});
|
|
918
903
|
context.busy = true;
|
|
919
904
|
try {
|
|
920
|
-
return await afterEpochReport(context, () => workerPass(context, dependencies, configuration));
|
|
905
|
+
return await afterEpochReport(context, () => workerPass(context, dependencies, configuration, reach));
|
|
921
906
|
} finally {
|
|
922
907
|
context.busy = false;
|
|
923
908
|
if (context.deferredRemoval) context.onIdle?.();
|
|
@@ -928,8 +913,23 @@ export async function runWorkers(options, dependencies = {}) {
|
|
|
928
913
|
const { context, tmux } = await workerContext(options, dependencies);
|
|
929
914
|
const { session, runtime } = context;
|
|
930
915
|
const supervised = await maintainControl(session, async (signal) => {
|
|
931
|
-
|
|
932
|
-
|
|
916
|
+
const watcher = new AbortController();
|
|
917
|
+
const commanded = (subject) => {
|
|
918
|
+
if (subject === null || subject === "commands") context.commands.notify();
|
|
919
|
+
};
|
|
920
|
+
const watching = (dependencies.watchKitWork ?? watchKitWork)(
|
|
921
|
+
session,
|
|
922
|
+
AbortSignal.any([signal, watcher.signal]),
|
|
923
|
+
{ available: commanded, changed: () => {} },
|
|
924
|
+
dependencies,
|
|
925
|
+
).catch((error) => reported(`control stream ended: ${error?.code ?? error?.message ?? error}`));
|
|
926
|
+
try {
|
|
927
|
+
await reportedWorkingCopies(applyWorkingCopies, context, signal);
|
|
928
|
+
return await controlledPass(context, dependencies, context.configuration, signal);
|
|
929
|
+
} finally {
|
|
930
|
+
watcher.abort();
|
|
931
|
+
await watching;
|
|
932
|
+
}
|
|
933
933
|
}, session.signal);
|
|
934
934
|
return {
|
|
935
935
|
tmux,
|
|
@@ -950,65 +950,11 @@ function reported(line) {
|
|
|
950
950
|
}
|
|
951
951
|
|
|
952
952
|
async function reportedWorkingCopies(act, context, signal) {
|
|
953
|
-
|
|
953
|
+
const outcome = await act(context, signal);
|
|
954
|
+
for (const refused of outcome.refused) {
|
|
954
955
|
reported(`working copy ${refused.path} refused: ${refused.code}`);
|
|
955
956
|
}
|
|
956
|
-
|
|
957
|
-
|
|
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
|
-
};
|
|
957
|
+
return outcome;
|
|
1012
958
|
}
|
|
1013
959
|
|
|
1014
960
|
function residentUpdate(context, wake) {
|
|
@@ -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;
|
|
@@ -1143,50 +1092,111 @@ export async function runResident(options, dependencies = {}) {
|
|
|
1143
1092
|
context.startLogins(needsLogin);
|
|
1144
1093
|
});
|
|
1145
1094
|
const unavailable = (error) => reported(`working copies unavailable: ${error?.message ?? error}`);
|
|
1146
|
-
const
|
|
1147
|
-
|
|
1095
|
+
const changed = () => {
|
|
1096
|
+
if (!update.requested) deliveries.trigger();
|
|
1097
|
+
};
|
|
1098
|
+
const behind = (outcome) => {
|
|
1099
|
+
if (!outcome.behind) return;
|
|
1100
|
+
controlSleep(interval, signal).then(changed, () => {});
|
|
1101
|
+
};
|
|
1102
|
+
const copies = coalescedTask(signal, unavailable, async () =>
|
|
1103
|
+
behind(await reportedWorkingCopies(applyWorkingCopies, context, signal)),
|
|
1148
1104
|
);
|
|
1149
|
-
const deliveries = coalescedTask(signal, unavailable, () =>
|
|
1150
|
-
reportedWorkingCopies(deliverWorkingCopies, context, signal),
|
|
1105
|
+
const deliveries = coalescedTask(signal, unavailable, async () =>
|
|
1106
|
+
behind(await reportedWorkingCopies(deliverWorkingCopies, context, signal)),
|
|
1151
1107
|
);
|
|
1108
|
+
sources = sourceEntries({
|
|
1109
|
+
session: context.session,
|
|
1110
|
+
directory: context.directory,
|
|
1111
|
+
dependencies,
|
|
1112
|
+
signal: AbortSignal.any([signal, watcher.signal]),
|
|
1113
|
+
now: session.now,
|
|
1114
|
+
draining: () => update.requested,
|
|
1115
|
+
unavailable: (error) => reported(`google sources unavailable: ${error?.message ?? error}`),
|
|
1116
|
+
});
|
|
1117
|
+
await sources.start();
|
|
1152
1118
|
const pointer = () => {
|
|
1153
1119
|
if (!update.requested) agentRefresh.trigger();
|
|
1154
1120
|
};
|
|
1121
|
+
let passOwed = null;
|
|
1122
|
+
let deferred = null;
|
|
1123
|
+
let retrying = null;
|
|
1124
|
+
const higher = (reach, than) =>
|
|
1125
|
+
PASS_REACHES.indexOf(reach) > PASS_REACHES.indexOf(than) ? reach : than;
|
|
1126
|
+
const owe = (reach) => {
|
|
1127
|
+
passOwed = higher(reach, passOwed);
|
|
1128
|
+
wake.notify();
|
|
1129
|
+
};
|
|
1130
|
+
const defer = (reach) => {
|
|
1131
|
+
deferred = higher(reach, deferred);
|
|
1132
|
+
if (retrying !== null) return;
|
|
1133
|
+
retrying = new AbortController();
|
|
1134
|
+
controlSleep(interval, AbortSignal.any([signal, retrying.signal])).then(() => {
|
|
1135
|
+
retrying = null;
|
|
1136
|
+
const owed = deferred;
|
|
1137
|
+
deferred = null;
|
|
1138
|
+
owe(owed);
|
|
1139
|
+
}, () => {});
|
|
1140
|
+
};
|
|
1141
|
+
const settle = (reach) => {
|
|
1142
|
+
retrying?.abort();
|
|
1143
|
+
retrying = null;
|
|
1144
|
+
if (higher(deferred, reach) !== reach) owe(deferred);
|
|
1145
|
+
deferred = null;
|
|
1146
|
+
};
|
|
1155
1147
|
const available = (subject) => {
|
|
1148
|
+
if (subject === null || subject === "runs") owe("runs");
|
|
1149
|
+
if (subject === null || subject === "commands") {
|
|
1150
|
+
context.commands.notify();
|
|
1151
|
+
owe("commands");
|
|
1152
|
+
}
|
|
1156
1153
|
if (update.requested) return;
|
|
1157
|
-
if (subject
|
|
1158
|
-
if (subject
|
|
1154
|
+
if (subject === null || subject === "agents") agentRefresh.trigger();
|
|
1155
|
+
if (subject === null || subject === "working_copy") copies.trigger();
|
|
1156
|
+
if (subject === null || subject === "sources") sources.reconcile();
|
|
1159
1157
|
};
|
|
1160
1158
|
context.onIdle = pointer;
|
|
1161
1159
|
context.workingCopiesApplied = copies.settled;
|
|
1162
|
-
if (!update.requested) copies.trigger();
|
|
1163
1160
|
const watching = (dependencies.watchKitWork ?? watchKitWork)(
|
|
1164
1161
|
context.session,
|
|
1165
1162
|
AbortSignal.any([signal, watcher.signal]),
|
|
1166
|
-
available,
|
|
1163
|
+
{ available, changed },
|
|
1167
1164
|
dependencies,
|
|
1168
1165
|
).catch(fail);
|
|
1169
1166
|
try {
|
|
1170
1167
|
while (!signal.aborted) {
|
|
1171
1168
|
if (failure !== null) throw failure;
|
|
1172
|
-
if (!session.refused) {
|
|
1169
|
+
if (passOwed !== null && !session.refused) {
|
|
1170
|
+
const reach = passOwed;
|
|
1171
|
+
passOwed = null;
|
|
1173
1172
|
const configuration = await readConfiguration(options.configuration);
|
|
1174
|
-
await controlledPass(context, dependencies, configuration, signal).catch(
|
|
1175
|
-
|
|
1176
|
-
|
|
1173
|
+
const passed = await controlledPass(context, dependencies, configuration, signal, reach).catch(
|
|
1174
|
+
(error) => {
|
|
1175
|
+
if (!session.refused) throw error;
|
|
1176
|
+
return null;
|
|
1177
|
+
},
|
|
1178
|
+
);
|
|
1179
|
+
if ((passed?.run ?? null) !== null) owe(reach);
|
|
1180
|
+
if ((passed?.unresolved.length ?? 0) > 0) defer(reach);
|
|
1181
|
+
else if (passed !== null) settle(reach);
|
|
1177
1182
|
}
|
|
1178
|
-
|
|
1183
|
+
const draining = session.refused || update.requested;
|
|
1184
|
+
if (draining) {
|
|
1179
1185
|
const sessions = await heldSessions(context);
|
|
1180
1186
|
if (sessions.length > 0) update.answer({ ready: false, sessions });
|
|
1181
1187
|
else if (
|
|
1182
1188
|
session.refused ||
|
|
1183
|
-
(!served.serving() &&
|
|
1189
|
+
(!served.serving() &&
|
|
1190
|
+
logins.size === 0 &&
|
|
1191
|
+
agentRefresh.idle() &&
|
|
1192
|
+
copies.idle() &&
|
|
1193
|
+
deliveries.idle() &&
|
|
1194
|
+
sources.idle())
|
|
1184
1195
|
) {
|
|
1185
1196
|
return;
|
|
1186
1197
|
}
|
|
1187
1198
|
}
|
|
1188
|
-
await wake.wait(interval, signal);
|
|
1189
|
-
if (!update.requested && !session.refused) deliveries.trigger();
|
|
1199
|
+
if (session.refused || passOwed === null) await wake.wait(draining ? interval : Infinity, signal);
|
|
1190
1200
|
}
|
|
1191
1201
|
} finally {
|
|
1192
1202
|
watcher.abort();
|
|
@@ -1194,6 +1204,7 @@ export async function runResident(options, dependencies = {}) {
|
|
|
1194
1204
|
await agentRefresh.settled();
|
|
1195
1205
|
await copies.settled();
|
|
1196
1206
|
await deliveries.settled();
|
|
1207
|
+
await sources.settled();
|
|
1197
1208
|
for (const running of logins.values()) running.controller.abort();
|
|
1198
1209
|
await Promise.all([...logins.values()].map((running) => running.task));
|
|
1199
1210
|
await context.agentRefresh;
|
|
@@ -1219,7 +1230,7 @@ export async function runResident(options, dependencies = {}) {
|
|
|
1219
1230
|
await wake.wait(interval, options.signal);
|
|
1220
1231
|
}
|
|
1221
1232
|
update.quiesce();
|
|
1222
|
-
while (!options.signal.aborted) await wake.wait(
|
|
1233
|
+
while (!options.signal.aborted) await wake.wait(Infinity, options.signal);
|
|
1223
1234
|
throw new KitRefusal("kit_command_cancelled");
|
|
1224
1235
|
} finally {
|
|
1225
1236
|
await served.close();
|
package/src/working-copy.js
CHANGED
|
@@ -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 };
|
|
@@ -511,7 +497,7 @@ async function deliver(context, path, authority, signal) {
|
|
|
511
497
|
let copy = await readCopy(path);
|
|
512
498
|
if (copy?.active && JSON.stringify(copy.received.declared) !== JSON.stringify(declaration(authority))) {
|
|
513
499
|
await writeCopy(context.directory, await receive(context, copy, scopeOf(copy), signal), signal);
|
|
514
|
-
return;
|
|
500
|
+
return false;
|
|
515
501
|
}
|
|
516
502
|
while (
|
|
517
503
|
copy?.active &&
|
|
@@ -522,24 +508,26 @@ async function deliver(context, path, authority, signal) {
|
|
|
522
508
|
try {
|
|
523
509
|
view = await enumerated(context, copy, since, signal);
|
|
524
510
|
} catch (error) {
|
|
525
|
-
if (error.code === "replica_behind") return;
|
|
511
|
+
if (error.code === "replica_behind") return true;
|
|
526
512
|
if (error.code !== "position_expired") throw error;
|
|
527
513
|
await writeCopy(context.directory, await receive(context, copy, scopeOf(copy), signal), signal);
|
|
528
|
-
return;
|
|
514
|
+
return false;
|
|
529
515
|
}
|
|
530
|
-
if (view.position === since.position) return;
|
|
516
|
+
if (view.position === since.position) return false;
|
|
531
517
|
copy = await incorporated(copy, view, authority, signal);
|
|
532
518
|
await writeCopy(context.directory, copy, signal);
|
|
533
519
|
}
|
|
520
|
+
return false;
|
|
534
521
|
}
|
|
535
522
|
|
|
536
523
|
async function delivering(context, signal) {
|
|
537
524
|
const copies = (await heldCopies(context.directory)).filter((copy) => copy.active);
|
|
538
|
-
if (copies.length === 0) return { refused: [], lost: false };
|
|
525
|
+
if (copies.length === 0) return { refused: [], lost: false, behind: false };
|
|
539
526
|
const prefixes = copies.flatMap((copy) => selectedPrefixes(scopeOf(copy)));
|
|
540
527
|
const discovery = discoveryOf(await door(context.session, KIT_DOOR_DISCOVER_PATH, { prefixes }, signal));
|
|
541
528
|
const refused = [];
|
|
542
529
|
let lost = false;
|
|
530
|
+
let behind = false;
|
|
543
531
|
for (const copy of copies) {
|
|
544
532
|
const at = housePath(scopeOf(copy));
|
|
545
533
|
const authority = discovery.authorities.find((entry) => entry?.room_ref === copy.room_ref);
|
|
@@ -553,18 +541,24 @@ async function delivering(context, signal) {
|
|
|
553
541
|
}
|
|
554
542
|
const path = copyPath(context.directory, copy);
|
|
555
543
|
try {
|
|
556
|
-
await exclusively(path, () => deliver(context, path, authority, signal));
|
|
544
|
+
if (await exclusively(path, () => deliver(context, path, authority, signal))) behind = true;
|
|
557
545
|
} catch (error) {
|
|
558
546
|
if (!scopeRefused(error)) throw error;
|
|
559
547
|
refused.push({ path: at, code: error.code });
|
|
560
548
|
}
|
|
561
549
|
}
|
|
562
|
-
return { refused, lost };
|
|
550
|
+
return { refused, lost, behind };
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function deliveredOutcome(refused, behind) {
|
|
554
|
+
return behind ? { refused, behind } : { refused };
|
|
563
555
|
}
|
|
564
556
|
|
|
565
557
|
export async function deliverWorkingCopies(context, signal) {
|
|
566
558
|
const delivered = await delivering(context, signal);
|
|
567
|
-
return delivered.lost
|
|
559
|
+
return delivered.lost
|
|
560
|
+
? applyWorkingCopies(context, signal)
|
|
561
|
+
: deliveredOutcome(delivered.refused, delivered.behind);
|
|
568
562
|
}
|
|
569
563
|
|
|
570
564
|
export async function applyWorkingCopies(context, signal) {
|
|
@@ -596,7 +590,8 @@ export async function applyWorkingCopies(context, signal) {
|
|
|
596
590
|
refused.push({ path: housePath(scope), code: error.code });
|
|
597
591
|
}
|
|
598
592
|
}
|
|
599
|
-
|
|
593
|
+
const delivered = await delivering(context, signal);
|
|
594
|
+
return deliveredOutcome([...refused, ...delivered.refused], delivered.behind);
|
|
600
595
|
}
|
|
601
596
|
|
|
602
597
|
export async function workingCopyPaths(directory) {
|
package/src/google/declared.js
DELETED
|
@@ -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}$/;
|
package/src/instance/google.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const GOOGLE_OAUTH_CLIENT = Object.freeze({"id":"911965338518-867gdvuddb7kdkjo3qvpv6gds369alb7.apps.googleusercontent.com","secret":"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"});
|