@agentshouse/kit 0.1.0-alpha.3 → 0.1.0-alpha.31
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 +722 -263
- package/bin/connect-linux.sh +200 -25
- package/bin/kit.js +179 -157
- package/bootstrap.json +17 -0
- package/package.json +7 -2
- package/src/adapter-mcp.js +76 -12
- package/src/agents.js +63 -8
- package/src/binding.js +52 -0
- package/src/blocks.js +2 -26
- package/src/bridge.js +28 -14
- package/src/bytes.js +19 -8
- package/src/configuration.js +13 -209
- package/src/connector-login.js +3 -84
- package/src/contract.js +36 -11
- package/src/delivery.js +4 -7
- package/src/directory.js +6 -26
- package/src/draft.js +4 -82
- package/src/git-main.js +138 -0
- package/src/git.js +62 -0
- 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/host.js +6 -2
- package/src/house-main.js +65 -8
- package/src/house.js +446 -127
- package/src/how-we-work.md +25 -0
- package/src/inbound.js +2 -18
- package/src/kit-work-stream.js +118 -0
- package/src/launcher.js +46 -10
- package/src/local-files.js +3 -11
- package/src/login.js +23 -11
- package/src/mcp.js +18 -0
- package/src/mount.js +13 -21
- package/src/original.js +4 -4
- package/src/pacing.js +41 -0
- package/src/plan.js +37 -45
- package/src/repository.js +3 -61
- package/src/session-bin/git +2 -0
- package/src/setup.js +14 -15
- package/src/skill-set/ask-matt/PHASE-BOUNDARIES.md +55 -0
- package/src/skill-set/ask-matt/SKILL.md +91 -0
- package/src/skill-set/ask-matt/agents/openai.yaml +4 -0
- package/src/skill-set/code-review/LICENSE +21 -0
- package/src/skill-set/code-review/SKILL.md +89 -0
- package/src/skill-set/code-review/agents/openai.yaml +3 -0
- package/src/skill-set/codebase-design/DEEPENING.md +37 -0
- package/src/skill-set/codebase-design/DESIGN-IT-TWICE.md +44 -0
- package/src/skill-set/codebase-design/LICENSE +21 -0
- package/src/skill-set/codebase-design/SKILL.md +116 -0
- package/src/skill-set/codebase-design/agents/openai.yaml +3 -0
- package/src/skill-set/diagnosing-bugs/LICENSE +21 -0
- package/src/skill-set/diagnosing-bugs/SKILL.md +140 -0
- package/src/skill-set/diagnosing-bugs/agents/openai.yaml +3 -0
- package/src/skill-set/diagnosing-bugs/scripts/hitl-loop.template.sh +44 -0
- package/src/skill-set/domain-modeling/ADR-FORMAT.md +47 -0
- package/src/skill-set/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/src/skill-set/domain-modeling/LICENSE +21 -0
- package/src/skill-set/domain-modeling/SKILL.md +76 -0
- package/src/skill-set/domain-modeling/agents/openai.yaml +3 -0
- package/src/skill-set/grill-me/LICENSE +21 -0
- package/src/skill-set/grill-me/SKILL.md +8 -0
- package/src/skill-set/grill-me/agents/openai.yaml +4 -0
- package/src/skill-set/grill-with-docs/LICENSE +21 -0
- package/src/skill-set/grill-with-docs/SKILL.md +8 -0
- package/src/skill-set/grill-with-docs/agents/openai.yaml +4 -0
- package/src/skill-set/grilling/LICENSE +21 -0
- package/src/{starter-set → skill-set}/grilling/SKILL.md +1 -1
- package/src/skill-set/handoff/LICENSE +21 -0
- package/src/skill-set/handoff/SKILL.md +17 -0
- package/src/skill-set/handoff/agents/openai.yaml +4 -0
- package/src/skill-set/implement/LICENSE +21 -0
- package/src/skill-set/implement/SKILL.md +16 -0
- package/src/skill-set/implement/agents/openai.yaml +4 -0
- package/src/skill-set/improve-codebase-architecture/HTML-REPORT.md +123 -0
- package/src/skill-set/improve-codebase-architecture/LICENSE +21 -0
- package/src/skill-set/improve-codebase-architecture/SKILL.md +72 -0
- package/src/skill-set/improve-codebase-architecture/agents/openai.yaml +4 -0
- package/src/skill-set/prototype/LICENSE +21 -0
- package/src/skill-set/prototype/LOGIC.md +67 -0
- package/src/skill-set/prototype/SKILL.md +28 -0
- package/src/skill-set/prototype/UI.md +112 -0
- package/src/skill-set/prototype/agents/openai.yaml +3 -0
- package/src/skill-set/research/LICENSE +21 -0
- package/src/skill-set/research/SKILL.md +14 -0
- package/src/skill-set/research/agents/openai.yaml +3 -0
- package/src/skill-set/resolving-merge-conflicts/LICENSE +21 -0
- package/src/skill-set/resolving-merge-conflicts/SKILL.md +16 -0
- package/src/skill-set/resolving-merge-conflicts/agents/openai.yaml +3 -0
- package/src/skill-set/tdd/LICENSE +21 -0
- package/src/skill-set/tdd/SKILL.md +40 -0
- package/src/skill-set/tdd/agents/openai.yaml +3 -0
- package/src/skill-set/tdd/mocking.md +59 -0
- package/src/skill-set/tdd/tests.md +77 -0
- package/src/skill-set/teach/GLOSSARY-FORMAT.md +35 -0
- package/src/skill-set/teach/LEARNING-RECORD-FORMAT.md +46 -0
- package/src/skill-set/teach/LICENSE +21 -0
- package/src/skill-set/teach/MISSION-FORMAT.md +31 -0
- package/src/skill-set/teach/RESOURCES-FORMAT.md +32 -0
- package/src/skill-set/teach/SKILL.md +141 -0
- package/src/skill-set/teach/agents/openai.yaml +4 -0
- package/src/skill-set/to-questionnaire/LICENSE +21 -0
- package/src/skill-set/to-questionnaire/SKILL.md +55 -0
- package/src/skill-set/to-questionnaire/agents/openai.yaml +4 -0
- package/src/skill-set/to-spec/LICENSE +21 -0
- package/src/skill-set/to-spec/SKILL.md +76 -0
- package/src/skill-set/to-spec/agents/openai.yaml +4 -0
- package/src/skill-set/to-tickets/LICENSE +21 -0
- package/src/skill-set/to-tickets/SKILL.md +106 -0
- package/src/skill-set/to-tickets/agents/openai.yaml +4 -0
- package/src/skill-set/wait-what/LICENSE +21 -0
- package/src/skill-set/wait-what/SKILL.md +8 -0
- package/src/skill-set/wait-what/agents/openai.yaml +4 -0
- package/src/skill-set/wayfinder/LICENSE +21 -0
- package/src/skill-set/wayfinder/SKILL.md +129 -0
- package/src/skill-set/wayfinder/agents/openai.yaml +4 -0
- package/src/skill-set/writing-for-agents/LICENSE +21 -0
- package/src/skill-set/writing-for-agents/SKILL-MECHANICS.md +22 -0
- package/src/skill-set/writing-for-agents/SKILL.md +83 -0
- package/src/skill-set/writing-for-agents/agents/openai.yaml +3 -0
- package/src/skill-set.js +85 -0
- package/src/source.js +93 -54
- package/src/state.js +19 -1
- package/src/sync-library.js +7 -11
- package/src/sync-state.js +16 -3
- package/src/sync.js +129 -41
- package/src/tasks.js +75 -0
- package/src/telegram-view.js +214 -0
- package/src/telegram.js +161 -66
- package/src/turn-frames.js +69 -0
- package/src/worker.js +685 -184
- package/src/working-copy.js +1221 -0
- package/src/workspace.js +40 -32
- package/src/google/declared.js +0 -146
- package/src/google/id.js +0 -1
- package/src/instance/google.js +0 -1
- package/src/starter-set.js +0 -17
- /package/src/{starter-set/grilling → skill-set/ask-matt}/LICENSE +0 -0
- /package/src/{starter-set → skill-set}/grilling/agents/openai.yaml +0 -0
package/src/sync.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
assertWritableChanges,
|
|
14
14
|
authorityFor,
|
|
15
15
|
deriveChanges,
|
|
16
|
+
housePath,
|
|
16
17
|
OPERATIONS,
|
|
17
18
|
relativeTo,
|
|
18
19
|
resultingFiles,
|
|
@@ -41,12 +42,12 @@ function sessionDependencies(options, dependencies) {
|
|
|
41
42
|
};
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
async function withSession(options, dependencies, operation) {
|
|
45
|
+
export async function withSession(options, dependencies, operation) {
|
|
45
46
|
const session = await openSession(options, sessionDependencies(options, dependencies));
|
|
46
47
|
return maintainControl(session, (signal) => operation(session, signal), options.signal);
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
async function door(session, path, body, signal) {
|
|
50
|
+
export async function door(session, path, body, signal) {
|
|
50
51
|
return houseRequest(
|
|
51
52
|
session.house,
|
|
52
53
|
await heldControl(session, signal),
|
|
@@ -83,7 +84,7 @@ function changePosition(value, declaration) {
|
|
|
83
84
|
return { position: value.position, log_epoch: value.log_epoch };
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
function discoveryOf(value) {
|
|
87
|
+
export function discoveryOf(value) {
|
|
87
88
|
if (typeof value?.md_model_version !== "string" || !Array.isArray(value.authorities)) {
|
|
88
89
|
throw new KitRefusal("kit_sync_rejected", "discover");
|
|
89
90
|
}
|
|
@@ -93,7 +94,7 @@ function discoveryOf(value) {
|
|
|
93
94
|
return value;
|
|
94
95
|
}
|
|
95
96
|
|
|
96
|
-
async function bootstrapBundle(session, prefixes, signal) {
|
|
97
|
+
export async function bootstrapBundle(session, prefixes, signal) {
|
|
97
98
|
const answer = await door(session, KIT_DOOR_BOOTSTRAP_PATH, { prefixes }, signal);
|
|
98
99
|
if (typeof answer?.transfer?.url === "string") {
|
|
99
100
|
const granted = await houseGetJson(answer.transfer.url, session.dependencies, signal);
|
|
@@ -105,15 +106,12 @@ async function bootstrapBundle(session, prefixes, signal) {
|
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
function matchedDiscovery(discovery, scope) {
|
|
108
|
-
if (scope.manifest?.scope === "private") {
|
|
109
|
-
return discovery.authorities.find((entry) => entry.kind === "private");
|
|
110
|
-
}
|
|
111
109
|
return discovery.authorities.find(
|
|
112
110
|
(entry) => entry.kind === "room" && entry.room_ref === scope.manifest?.anchor,
|
|
113
111
|
);
|
|
114
112
|
}
|
|
115
113
|
|
|
116
|
-
|
|
114
|
+
function scopeContents(replica, scope) {
|
|
117
115
|
if (
|
|
118
116
|
typeof scope?.manifest?.path !== "string" ||
|
|
119
117
|
typeof scope.manifest.position !== "string" ||
|
|
@@ -124,11 +122,18 @@ async function writeScope(replica, scope, signal) {
|
|
|
124
122
|
if (!Array.isArray(scope.contents) || !Array.isArray(scope.manifest.files)) {
|
|
125
123
|
throw new KitRefusal("kit_bootstrap_rejected");
|
|
126
124
|
}
|
|
127
|
-
const files = [];
|
|
128
125
|
for (const entry of scope.contents) {
|
|
129
126
|
if (typeof entry?.path !== "string" || typeof entry.content !== "string") {
|
|
130
127
|
throw new KitRefusal("kit_bootstrap_rejected");
|
|
131
128
|
}
|
|
129
|
+
locate(replica, scope.manifest.path, entry.path);
|
|
130
|
+
}
|
|
131
|
+
return scope.contents;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function writeScope(replica, scope, signal) {
|
|
135
|
+
const files = [];
|
|
136
|
+
for (const entry of scopeContents(replica, scope)) {
|
|
132
137
|
const meta = scope.manifest.files.find((file) => file.path === entry.path);
|
|
133
138
|
await writeReplicaFile(locate(replica, scope.manifest.path, entry.path), entry.content, signal);
|
|
134
139
|
files.push({
|
|
@@ -142,18 +147,21 @@ async function writeScope(replica, scope, signal) {
|
|
|
142
147
|
}
|
|
143
148
|
|
|
144
149
|
async function replaceScope(replica, previous, scope, signal) {
|
|
145
|
-
const
|
|
146
|
-
const kept = new Set(next.map((file) => file.path));
|
|
150
|
+
const kept = new Set(scopeContents(replica, scope).map((entry) => entry.path));
|
|
147
151
|
const root = locate(replica, scope.manifest.path);
|
|
148
152
|
for (const path of await listFiles(root)) {
|
|
149
|
-
if (!kept.has(path)) await removeReplicaFile(
|
|
153
|
+
if (!kept.has(path)) await removeReplicaFile(replica, scope.manifest.path, path);
|
|
150
154
|
}
|
|
151
155
|
if (previous !== undefined) {
|
|
152
156
|
for (const file of previous.files) {
|
|
153
|
-
if (!kept.has(file.path)) await removeReplicaFile(
|
|
157
|
+
if (!kept.has(file.path)) await removeReplicaFile(replica, previous.path, file.path);
|
|
154
158
|
}
|
|
155
159
|
}
|
|
156
|
-
return
|
|
160
|
+
return writeScope(replica, scope, signal);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function removalsFirst(entries, removal) {
|
|
164
|
+
return [...entries.filter(removal), ...entries.filter((entry) => !removal(entry))];
|
|
157
165
|
}
|
|
158
166
|
|
|
159
167
|
function stringList(value) {
|
|
@@ -173,11 +181,17 @@ function operationsOf(value) {
|
|
|
173
181
|
function authorityRecord(scope, discovery, files) {
|
|
174
182
|
const found = matchedDiscovery(discovery, scope);
|
|
175
183
|
const operations = found === undefined ? null : operationsOf(found.operations);
|
|
176
|
-
if (
|
|
184
|
+
if (
|
|
185
|
+
scope.manifest?.scope !== "room" ||
|
|
186
|
+
typeof scope.manifest.anchor !== "string" ||
|
|
187
|
+
found === undefined ||
|
|
188
|
+
!stringList(found.protected_paths) ||
|
|
189
|
+
operations === null
|
|
190
|
+
) {
|
|
177
191
|
throw new KitRefusal("kit_bootstrap_rejected");
|
|
178
192
|
}
|
|
179
193
|
return {
|
|
180
|
-
kind:
|
|
194
|
+
kind: "room",
|
|
181
195
|
authority: scope.manifest.anchor,
|
|
182
196
|
room_handle: scope.manifest.room_handle,
|
|
183
197
|
path: scope.manifest.path,
|
|
@@ -192,9 +206,10 @@ function authorityRecord(scope, discovery, files) {
|
|
|
192
206
|
async function materialize(options, discovery, bundle, signal, previous = null) {
|
|
193
207
|
const authorities = [];
|
|
194
208
|
for (const scope of bundle.scopes) {
|
|
209
|
+
const record = authorityRecord(scope, discovery, []);
|
|
195
210
|
const held = previous?.authorities.find((entry) => entry.authority === scope.manifest?.anchor);
|
|
196
211
|
const files = await replaceScope(options.replica, held, scope, signal);
|
|
197
|
-
authorities.push(
|
|
212
|
+
authorities.push({ ...record, files });
|
|
198
213
|
}
|
|
199
214
|
const state = {
|
|
200
215
|
version: EXPLICIT_SYNC_VERSION,
|
|
@@ -234,6 +249,12 @@ function enumeratedRecord(path, content, change) {
|
|
|
234
249
|
};
|
|
235
250
|
}
|
|
236
251
|
|
|
252
|
+
async function writeSidecar(options, authority, recorded, path, change, signal) {
|
|
253
|
+
const sidecar = sidecarPath(path);
|
|
254
|
+
await writeReplicaFile(locate(options.replica, authority.path, sidecar), change.provenance, signal);
|
|
255
|
+
recorded.set(sidecar, enumeratedRecord(sidecar, change.provenance, change));
|
|
256
|
+
}
|
|
257
|
+
|
|
237
258
|
async function applyEnumeration(options, state, enumeration, signal) {
|
|
238
259
|
if (!Array.isArray(enumeration?.authorities)) throw new KitRefusal("kit_sync_rejected", "enumerate");
|
|
239
260
|
for (const view of enumeration.authorities) {
|
|
@@ -241,10 +262,10 @@ async function applyEnumeration(options, state, enumeration, signal) {
|
|
|
241
262
|
if (authority === undefined) throw new KitRefusal("kit_sync_rejected", view.authority);
|
|
242
263
|
const reached = changePosition(view, "enumerate");
|
|
243
264
|
const recorded = new Map(authority.files.map((file) => [file.path, file]));
|
|
244
|
-
for (const change of view.changes ?? []) {
|
|
265
|
+
for (const change of removalsFirst(view.changes ?? [], (change) => change?.removed === true)) {
|
|
245
266
|
if (typeof change?.path !== "string") throw new KitRefusal("kit_sync_rejected", "enumerate");
|
|
246
267
|
if (change.removed === true) {
|
|
247
|
-
await removeReplicaFile(
|
|
268
|
+
await removeReplicaFile(options.replica, authority.path, change.path);
|
|
248
269
|
recorded.delete(change.path);
|
|
249
270
|
continue;
|
|
250
271
|
}
|
|
@@ -257,13 +278,7 @@ async function applyEnumeration(options, state, enumeration, signal) {
|
|
|
257
278
|
recorded.set(change.path, enumeratedRecord(change.path, change.content, change));
|
|
258
279
|
}
|
|
259
280
|
if (typeof change.provenance === "string") {
|
|
260
|
-
|
|
261
|
-
await writeReplicaFile(
|
|
262
|
-
locate(options.replica, authority.path, sidecar),
|
|
263
|
-
change.provenance,
|
|
264
|
-
signal,
|
|
265
|
-
);
|
|
266
|
-
recorded.set(sidecar, enumeratedRecord(sidecar, change.provenance, change));
|
|
281
|
+
await writeSidecar(options, authority, recorded, change.path, change, signal);
|
|
267
282
|
}
|
|
268
283
|
}
|
|
269
284
|
authority.files = [...recorded.values()];
|
|
@@ -295,13 +310,41 @@ function uncertainTransport(error) {
|
|
|
295
310
|
);
|
|
296
311
|
}
|
|
297
312
|
|
|
298
|
-
async function
|
|
313
|
+
async function servedProvenance(session, options, state, authority, signal) {
|
|
314
|
+
const enumeration = await replicaDoor(
|
|
315
|
+
session,
|
|
316
|
+
options,
|
|
317
|
+
state,
|
|
318
|
+
KIT_DOOR_ENUMERATE_PATH,
|
|
319
|
+
{
|
|
320
|
+
prefixes: state.prefixes.filter((prefix) => authorityFor(state, prefix) === authority),
|
|
321
|
+
positions: [
|
|
322
|
+
{ authority: authority.authority, position: authority.position, log_epoch: authority.log_epoch },
|
|
323
|
+
],
|
|
324
|
+
},
|
|
325
|
+
signal,
|
|
326
|
+
);
|
|
327
|
+
const view = Array.isArray(enumeration?.authorities)
|
|
328
|
+
? enumeration.authorities.find((entry) => entry?.authority === authority.authority)
|
|
329
|
+
: undefined;
|
|
330
|
+
if (!Array.isArray(view?.changes)) throw new KitRefusal("kit_sync_rejected", "enumerate");
|
|
331
|
+
return new Map(
|
|
332
|
+
view.changes
|
|
333
|
+
.filter((change) => typeof change?.provenance === "string")
|
|
334
|
+
.map((change) => [change.path, change]),
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function acceptBatch(session, options, state, outcome, signal, pending = null) {
|
|
299
339
|
const authority = state.authorities.find((entry) => entry.authority === outcome.authority);
|
|
300
340
|
if (authority === undefined) throw new KitRefusal("kit_sync_rejected", outcome.authority);
|
|
301
341
|
const reached = changePosition(outcome, "batch");
|
|
302
342
|
const recorded = new Map(authority.files.map((file) => [file.path, file]));
|
|
303
343
|
const predicted = predictedReplica(authority, pending);
|
|
304
|
-
|
|
344
|
+
const provenance = (outcome.contents ?? []).some((file) => typeof file?.content === "string")
|
|
345
|
+
? await servedProvenance(session, options, state, authority, signal)
|
|
346
|
+
: new Map();
|
|
347
|
+
for (const file of removalsFirst(outcome.contents ?? [], (file) => file?.content === null)) {
|
|
305
348
|
const relative = relativeTo(authority.path, file.path);
|
|
306
349
|
const destination = locate(options.replica, authority.path, relative);
|
|
307
350
|
const local = await replicaText(destination);
|
|
@@ -315,7 +358,7 @@ async function acceptBatch(options, state, outcome, signal, pending = null) {
|
|
|
315
358
|
local !== accepted &&
|
|
316
359
|
local !== expected;
|
|
317
360
|
if (accepted === null) {
|
|
318
|
-
if (!preserve) await removeReplicaFile(
|
|
361
|
+
if (!preserve) await removeReplicaFile(options.replica, authority.path, relative);
|
|
319
362
|
recorded.delete(relative);
|
|
320
363
|
continue;
|
|
321
364
|
}
|
|
@@ -326,6 +369,10 @@ async function acceptBatch(options, state, outcome, signal, pending = null) {
|
|
|
326
369
|
sha256: "",
|
|
327
370
|
content: accepted,
|
|
328
371
|
});
|
|
372
|
+
const served = provenance.get(relative);
|
|
373
|
+
if (served !== undefined && served.revision === file.revision) {
|
|
374
|
+
await writeSidecar(options, authority, recorded, relative, served, signal);
|
|
375
|
+
}
|
|
329
376
|
}
|
|
330
377
|
authority.files = [...recorded.values()];
|
|
331
378
|
Object.assign(authority, reached);
|
|
@@ -337,16 +384,65 @@ function sameBody(left, right) {
|
|
|
337
384
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
338
385
|
}
|
|
339
386
|
|
|
387
|
+
function contentMap(authority) {
|
|
388
|
+
return Object.fromEntries(authority.files.map((file) => [file.path, file.content]));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function pullConfirmed(session, options, state, signal) {
|
|
392
|
+
const enumeration = await replicaDoor(
|
|
393
|
+
session,
|
|
394
|
+
options,
|
|
395
|
+
state,
|
|
396
|
+
KIT_DOOR_ENUMERATE_PATH,
|
|
397
|
+
{ prefixes: state.prefixes, positions: positionsOf(state) },
|
|
398
|
+
signal,
|
|
399
|
+
);
|
|
400
|
+
return applyEnumeration(options, state, enumeration, signal);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async function settleExpired(session, options, state, pending, signal) {
|
|
404
|
+
const authority = authorityFor(state, pending.body.changes[0].path);
|
|
405
|
+
const predicted = predictedReplica(authority, pending);
|
|
406
|
+
const before = contentMap(authority);
|
|
407
|
+
await pullConfirmed(session, options, state, signal);
|
|
408
|
+
const confirmed = contentMap(authority);
|
|
409
|
+
const touched = [...new Set([...Object.keys(before), ...Object.keys(predicted)])]
|
|
410
|
+
.filter((path) => before[path] !== predicted[path])
|
|
411
|
+
.sort();
|
|
412
|
+
const landed = touched.length > 0 && touched.every((path) => confirmed[path] === predicted[path]);
|
|
413
|
+
await clearPending(options.directory, state.directory);
|
|
414
|
+
if (!landed) {
|
|
415
|
+
return { pending, accepted: await sendBatch(session, options, state, pending.body.changes, signal) };
|
|
416
|
+
}
|
|
417
|
+
const recorded = new Map(authority.files.map((file) => [file.path, file]));
|
|
418
|
+
return {
|
|
419
|
+
pending,
|
|
420
|
+
accepted: {
|
|
421
|
+
authority: authority.authority,
|
|
422
|
+
position: authority.position,
|
|
423
|
+
log_epoch: authority.log_epoch,
|
|
424
|
+
contents: touched.map((path) => ({
|
|
425
|
+
path: housePath(authority.path, path),
|
|
426
|
+
content: recorded.get(path)?.content ?? null,
|
|
427
|
+
revision: recorded.get(path)?.revision ?? null,
|
|
428
|
+
})),
|
|
429
|
+
},
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
340
433
|
async function recoverPending(session, options, state, signal) {
|
|
341
434
|
const pending = await readPending(options.directory, state.directory);
|
|
342
435
|
if (pending === null) return null;
|
|
343
436
|
try {
|
|
344
437
|
const outcome = await replicaDoor(session, options, state, pending.path, pending.body, signal);
|
|
345
|
-
const accepted = await acceptBatch(options, state, outcome, signal, pending);
|
|
438
|
+
const accepted = await acceptBatch(session, options, state, outcome, signal, pending);
|
|
346
439
|
await clearPending(options.directory, state.directory);
|
|
347
440
|
return { pending, accepted };
|
|
348
441
|
} catch (error) {
|
|
349
442
|
if (uncertainTransport(error)) throw error;
|
|
443
|
+
if (error.code === "operation_expired") {
|
|
444
|
+
return settleExpired(session, options, state, pending, signal);
|
|
445
|
+
}
|
|
350
446
|
await clearPending(options.directory, state.directory);
|
|
351
447
|
throw error;
|
|
352
448
|
}
|
|
@@ -382,7 +478,7 @@ async function sendBatch(session, options, state, changes, signal) {
|
|
|
382
478
|
);
|
|
383
479
|
try {
|
|
384
480
|
const outcome = await replicaDoor(session, options, state, KIT_DOOR_BATCH_PATH, body, signal);
|
|
385
|
-
last = await acceptBatch(options, state, outcome, signal);
|
|
481
|
+
last = await acceptBatch(session, options, state, outcome, signal);
|
|
386
482
|
await clearPending(options.directory, state.directory);
|
|
387
483
|
} catch (error) {
|
|
388
484
|
if (uncertainTransport(error)) throw error;
|
|
@@ -409,6 +505,7 @@ export async function bootstrap(options, dependencies = {}) {
|
|
|
409
505
|
const canonical = await canonicalReplica(options.replica);
|
|
410
506
|
const existing = await readReplicaState(options.directory, canonical);
|
|
411
507
|
if (existing !== null) throw new KitRefusal("kit_sync_already_bootstrapped", canonical);
|
|
508
|
+
await clearPending(options.directory, canonical);
|
|
412
509
|
return withSession(options, dependencies, async (session, signal) => {
|
|
413
510
|
const discovery = discoveryOf(
|
|
414
511
|
await door(session, KIT_DOOR_DISCOVER_PATH, { prefixes: options.prefixes }, signal),
|
|
@@ -426,16 +523,7 @@ export async function bootstrap(options, dependencies = {}) {
|
|
|
426
523
|
|
|
427
524
|
export async function pull(options, dependencies = {}) {
|
|
428
525
|
return withSession(options, dependencies, async (session, signal) => {
|
|
429
|
-
const
|
|
430
|
-
const enumeration = await replicaDoor(
|
|
431
|
-
session,
|
|
432
|
-
options,
|
|
433
|
-
state,
|
|
434
|
-
KIT_DOOR_ENUMERATE_PATH,
|
|
435
|
-
{ prefixes: state.prefixes, positions: positionsOf(state) },
|
|
436
|
-
signal,
|
|
437
|
-
);
|
|
438
|
-
const next = await applyEnumeration(options, state, enumeration, signal);
|
|
526
|
+
const next = await pullConfirmed(session, options, await loadState(options), signal);
|
|
439
527
|
return { positions: positionsOf(next) };
|
|
440
528
|
});
|
|
441
529
|
}
|
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
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
TELEGRAM_RICH_MESSAGE_BLOCKS_MAX,
|
|
4
|
+
TELEGRAM_RICH_MESSAGE_CHARACTERS_MAX,
|
|
5
|
+
} from "./contract.js";
|
|
6
|
+
|
|
7
|
+
const PARTS = Object.freeze({ draft: "blocks", plan: "steps" });
|
|
8
|
+
const UNCOUNTED = new Set(["type", "language"]);
|
|
9
|
+
|
|
10
|
+
function richCharacters(value, key = null) {
|
|
11
|
+
if (typeof value === "string") return UNCOUNTED.has(key) ? 0 : value.length;
|
|
12
|
+
if (Array.isArray(value)) return value.reduce((sum, item) => sum + richCharacters(item), 0);
|
|
13
|
+
if (typeof value === "object" && value !== null) {
|
|
14
|
+
return Object.entries(value).reduce((sum, [name, item]) => sum + richCharacters(item, name), 0);
|
|
15
|
+
}
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function richText(value, key = null) {
|
|
20
|
+
if (typeof value === "string") return UNCOUNTED.has(key) ? [] : [value];
|
|
21
|
+
if (Array.isArray(value)) return value.flatMap((item) => richText(item));
|
|
22
|
+
if (typeof value === "object" && value !== null) {
|
|
23
|
+
return Object.entries(value).flatMap(([name, item]) => richText(item, name));
|
|
24
|
+
}
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function slices(text) {
|
|
29
|
+
const pieces = [];
|
|
30
|
+
let start = 0;
|
|
31
|
+
while (start < text.length) {
|
|
32
|
+
let end = Math.min(start + TELEGRAM_RICH_MESSAGE_CHARACTERS_MAX, text.length);
|
|
33
|
+
const last = text.charCodeAt(end - 1);
|
|
34
|
+
if (end < text.length && last >= 0xd800 && last <= 0xdbff) end -= 1;
|
|
35
|
+
pieces.push(text.slice(start, end));
|
|
36
|
+
start = end;
|
|
37
|
+
}
|
|
38
|
+
return pieces;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function grouped(block, listed) {
|
|
42
|
+
const groups = [];
|
|
43
|
+
let group = [];
|
|
44
|
+
let characters = 0;
|
|
45
|
+
for (const entry of block[listed]) {
|
|
46
|
+
const length = richCharacters(entry);
|
|
47
|
+
if (group.length > 0 && characters + length > TELEGRAM_RICH_MESSAGE_CHARACTERS_MAX) {
|
|
48
|
+
groups.push(group);
|
|
49
|
+
group = [];
|
|
50
|
+
characters = 0;
|
|
51
|
+
}
|
|
52
|
+
group.push(entry);
|
|
53
|
+
characters += length;
|
|
54
|
+
}
|
|
55
|
+
if (group.length > 0) groups.push(group);
|
|
56
|
+
return groups.flatMap((entries) => fitted({ ...block, [listed]: entries }));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function fitted(block) {
|
|
60
|
+
if (richCharacters(block) <= TELEGRAM_RICH_MESSAGE_CHARACTERS_MAX) return [block];
|
|
61
|
+
if (typeof block.text === "string") return slices(block.text).map((text) => ({ ...block, text }));
|
|
62
|
+
if (typeof block.expression === "string") {
|
|
63
|
+
return slices(block.expression).map((expression) => ({ ...block, expression }));
|
|
64
|
+
}
|
|
65
|
+
for (const listed of ["items", "cells"]) {
|
|
66
|
+
if (Array.isArray(block[listed]) && block[listed].length > 1) return grouped(block, listed);
|
|
67
|
+
}
|
|
68
|
+
return slices(richText(block).join("\n")).map((text) => ({ type: "paragraph", text }));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function richPieces(message) {
|
|
72
|
+
const pieces = [];
|
|
73
|
+
let blocks = [];
|
|
74
|
+
let characters = 0;
|
|
75
|
+
for (const part of message.blocks.flatMap(fitted)) {
|
|
76
|
+
const length = richCharacters(part);
|
|
77
|
+
if (
|
|
78
|
+
blocks.length > 0 &&
|
|
79
|
+
(blocks.length >= TELEGRAM_RICH_MESSAGE_BLOCKS_MAX ||
|
|
80
|
+
characters + length > TELEGRAM_RICH_MESSAGE_CHARACTERS_MAX)
|
|
81
|
+
) {
|
|
82
|
+
pieces.push({ ...message, blocks });
|
|
83
|
+
blocks = [];
|
|
84
|
+
characters = 0;
|
|
85
|
+
}
|
|
86
|
+
blocks.push(part);
|
|
87
|
+
characters += length;
|
|
88
|
+
}
|
|
89
|
+
pieces.push({ ...message, blocks });
|
|
90
|
+
return pieces;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function listItems(items, ordered) {
|
|
94
|
+
return items.map((item, index) => ({
|
|
95
|
+
blocks: [{ type: "paragraph", text: item }],
|
|
96
|
+
...(ordered ? { value: index + 1, type: "1" } : {}),
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function telegramBlock(block) {
|
|
101
|
+
if (typeof block !== "object" || block === null) return null;
|
|
102
|
+
if (block.type === "paragraph") return { type: "paragraph", text: block.text };
|
|
103
|
+
if (block.type === "heading") return { type: "heading", size: block.level, text: block.text };
|
|
104
|
+
if (block.type === "code") {
|
|
105
|
+
return {
|
|
106
|
+
type: "pre",
|
|
107
|
+
...(typeof block.language === "string" ? { language: block.language } : {}),
|
|
108
|
+
text: block.text,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (block.type === "list" && Array.isArray(block.items)) {
|
|
112
|
+
return { type: "list", items: listItems(block.items, block.ordered === true) };
|
|
113
|
+
}
|
|
114
|
+
if (block.type === "table" && Array.isArray(block.headers) && Array.isArray(block.rows)) {
|
|
115
|
+
return {
|
|
116
|
+
type: "table",
|
|
117
|
+
cells: [
|
|
118
|
+
block.headers.map((text) => ({ text, is_header: true })),
|
|
119
|
+
...block.rows.map((row) => (Array.isArray(row) ? row : []).map((text) => ({ text }))),
|
|
120
|
+
],
|
|
121
|
+
is_bordered: true,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (block.type === "formula") return { type: "mathematical_expression", expression: block.expression };
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function planBlocks(steps) {
|
|
129
|
+
return [
|
|
130
|
+
{ type: "heading", text: "Plan", size: 3 },
|
|
131
|
+
{
|
|
132
|
+
type: "list",
|
|
133
|
+
items: steps.map((step, index) => ({
|
|
134
|
+
blocks: [{ type: "paragraph", text: [`${step.label} — `, { type: "bold", text: step.status }] }],
|
|
135
|
+
value: index + 1,
|
|
136
|
+
type: "1",
|
|
137
|
+
})),
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function draftId(turn) {
|
|
143
|
+
return (createHash("sha256").update(turn).digest().readUInt32BE(0) % 2_147_483_647) + 1;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function viewFrame(frame) {
|
|
147
|
+
if (typeof frame !== "object" || frame === null || !Object.hasOwn(PARTS, frame.type)) return null;
|
|
148
|
+
for (const named of ["conversation_id", "turn_id"]) {
|
|
149
|
+
if (typeof frame[named] !== "string" || frame[named] === "") return null;
|
|
150
|
+
}
|
|
151
|
+
if (!Number.isSafeInteger(frame.sequence) || frame.sequence < 1) return null;
|
|
152
|
+
if (!Number.isSafeInteger(frame.from) || frame.from < 0) return null;
|
|
153
|
+
return Array.isArray(frame[PARTS[frame.type]]) ? frame : null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function turnViews(options) {
|
|
157
|
+
let threads = new Map();
|
|
158
|
+
const views = new Map();
|
|
159
|
+
|
|
160
|
+
function applied(view, frame) {
|
|
161
|
+
const items = frame[PARTS[frame.type]];
|
|
162
|
+
const held = view[frame.type];
|
|
163
|
+
if (held !== null && frame.sequence <= held.sequence) return false;
|
|
164
|
+
if (frame.from === 0) {
|
|
165
|
+
view[frame.type] = { sequence: frame.sequence, items };
|
|
166
|
+
view.asked = false;
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
if (held === null || frame.sequence !== held.sequence + 1 || frame.from > held.items.length) {
|
|
170
|
+
if (!view.asked) {
|
|
171
|
+
view.asked = true;
|
|
172
|
+
options.gap({ type: "gap", conversation_id: frame.conversation_id, turn_id: frame.turn_id });
|
|
173
|
+
}
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
view[frame.type] = { sequence: frame.sequence, items: [...held.items.slice(0, frame.from), ...items] };
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
address(listed) {
|
|
182
|
+
threads = new Map(listed.map((thread) => [thread.conversation_id, thread]));
|
|
183
|
+
for (const conversation of views.keys()) {
|
|
184
|
+
if (!threads.has(conversation)) views.delete(conversation);
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
receive(value) {
|
|
188
|
+
const frame = viewFrame(value);
|
|
189
|
+
if (frame === null || !threads.has(frame.conversation_id)) return null;
|
|
190
|
+
let view = views.get(frame.conversation_id);
|
|
191
|
+
if (view === undefined || view.turn !== frame.turn_id) {
|
|
192
|
+
view = { turn: frame.turn_id, draft: null, plan: null, asked: false };
|
|
193
|
+
views.set(frame.conversation_id, view);
|
|
194
|
+
}
|
|
195
|
+
return applied(view, frame) ? frame.conversation_id : null;
|
|
196
|
+
},
|
|
197
|
+
draft(conversation) {
|
|
198
|
+
const view = views.get(conversation);
|
|
199
|
+
const thread = threads.get(conversation);
|
|
200
|
+
if (view === undefined || thread === undefined) return null;
|
|
201
|
+
const blocks = [
|
|
202
|
+
...(view.plan === null ? [] : planBlocks(view.plan.items)),
|
|
203
|
+
...(view.draft === null ? [] : view.draft.items.map(telegramBlock).filter((block) => block !== null)),
|
|
204
|
+
];
|
|
205
|
+
if (blocks.length === 0) return null;
|
|
206
|
+
return {
|
|
207
|
+
chat_id: thread.chat_id,
|
|
208
|
+
topic_id: thread.topic_id,
|
|
209
|
+
draft_id: draftId(view.turn),
|
|
210
|
+
rich_message: richPieces({ blocks })[0],
|
|
211
|
+
};
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|