@agentshouse/kit 0.1.0-alpha.4 → 0.1.0-alpha.40
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 +41 -1671
- package/bin/connect-linux.sh +501 -53
- package/bin/connect-windows.ps1 +430 -0
- package/bin/kit.js +200 -159
- package/bootstrap.json +28 -0
- package/docs/configuration.md +104 -0
- package/docs/google.md +33 -0
- package/docs/linux.md +77 -0
- package/docs/macos.md +26 -0
- package/docs/telegram.md +29 -0
- package/docs/windows.md +36 -0
- package/package.json +7 -2
- package/src/adapter-mcp.js +92 -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 +14 -211
- package/src/connector-login.js +3 -84
- package/src/contract.js +44 -12
- package/src/conversation-originals.js +1 -3
- 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 +349 -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 +120 -0
- package/src/launcher.js +46 -10
- package/src/local-files.js +3 -11
- package/src/login.js +27 -14
- package/src/mcp.js +18 -0
- package/src/mount.js +9 -18
- 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 +12 -3
- package/src/sync.js +142 -43
- package/src/tasks.js +92 -0
- package/src/telegram-view.js +217 -0
- package/src/telegram.js +204 -69
- package/src/turn-frames.js +69 -0
- package/src/worker.js +738 -189
- package/src/working-copy.js +1274 -0
- package/src/workspace.js +299 -88
- 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);
|
|
@@ -110,7 +111,7 @@ function matchedDiscovery(discovery, scope) {
|
|
|
110
111
|
);
|
|
111
112
|
}
|
|
112
113
|
|
|
113
|
-
|
|
114
|
+
function scopeContents(replica, scope) {
|
|
114
115
|
if (
|
|
115
116
|
typeof scope?.manifest?.path !== "string" ||
|
|
116
117
|
typeof scope.manifest.position !== "string" ||
|
|
@@ -121,36 +122,54 @@ async function writeScope(replica, scope, signal) {
|
|
|
121
122
|
if (!Array.isArray(scope.contents) || !Array.isArray(scope.manifest.files)) {
|
|
122
123
|
throw new KitRefusal("kit_bootstrap_rejected");
|
|
123
124
|
}
|
|
124
|
-
const files = [];
|
|
125
125
|
for (const entry of scope.contents) {
|
|
126
126
|
if (typeof entry?.path !== "string" || typeof entry.content !== "string") {
|
|
127
127
|
throw new KitRefusal("kit_bootstrap_rejected");
|
|
128
128
|
}
|
|
129
|
-
|
|
129
|
+
locate(replica, scope.manifest.path, entry.path);
|
|
130
|
+
}
|
|
131
|
+
return scope.contents;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function servedFile(scope, path) {
|
|
135
|
+
return scope.manifest.files.find((file) => file?.path === path);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function writeScope(replica, scope, signal) {
|
|
139
|
+
const recorded = new Map();
|
|
140
|
+
for (const entry of scopeContents(replica, scope)) {
|
|
141
|
+
const meta = servedFile(scope, entry.path) ?? {};
|
|
130
142
|
await writeReplicaFile(locate(replica, scope.manifest.path, entry.path), entry.content, signal);
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
content: entry.content,
|
|
136
|
-
});
|
|
143
|
+
recorded.set(entry.path, enumeratedRecord(entry.path, entry.content, meta));
|
|
144
|
+
if (typeof meta.provenance === "string") {
|
|
145
|
+
await writeSidecar({ replica }, scope.manifest, recorded, entry.path, meta, signal);
|
|
146
|
+
}
|
|
137
147
|
}
|
|
138
|
-
return
|
|
148
|
+
return [...recorded.values()];
|
|
139
149
|
}
|
|
140
150
|
|
|
141
151
|
async function replaceScope(replica, previous, scope, signal) {
|
|
142
|
-
const
|
|
143
|
-
|
|
152
|
+
const kept = new Set(
|
|
153
|
+
scopeContents(replica, scope).flatMap((entry) =>
|
|
154
|
+
typeof servedFile(scope, entry.path)?.provenance === "string"
|
|
155
|
+
? [entry.path, sidecarPath(entry.path)]
|
|
156
|
+
: [entry.path],
|
|
157
|
+
),
|
|
158
|
+
);
|
|
144
159
|
const root = locate(replica, scope.manifest.path);
|
|
145
160
|
for (const path of await listFiles(root)) {
|
|
146
|
-
if (!kept.has(path)) await removeReplicaFile(
|
|
161
|
+
if (!kept.has(path)) await removeReplicaFile(replica, scope.manifest.path, path);
|
|
147
162
|
}
|
|
148
163
|
if (previous !== undefined) {
|
|
149
164
|
for (const file of previous.files) {
|
|
150
|
-
if (!kept.has(file.path)) await removeReplicaFile(
|
|
165
|
+
if (!kept.has(file.path)) await removeReplicaFile(replica, previous.path, file.path);
|
|
151
166
|
}
|
|
152
167
|
}
|
|
153
|
-
return
|
|
168
|
+
return writeScope(replica, scope, signal);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function removalsFirst(entries, removal) {
|
|
172
|
+
return [...entries.filter(removal), ...entries.filter((entry) => !removal(entry))];
|
|
154
173
|
}
|
|
155
174
|
|
|
156
175
|
function stringList(value) {
|
|
@@ -238,6 +257,18 @@ function enumeratedRecord(path, content, change) {
|
|
|
238
257
|
};
|
|
239
258
|
}
|
|
240
259
|
|
|
260
|
+
async function writeSidecar(options, authority, recorded, path, change, signal) {
|
|
261
|
+
const sidecar = sidecarPath(path);
|
|
262
|
+
await writeReplicaFile(locate(options.replica, authority.path, sidecar), change.provenance, signal);
|
|
263
|
+
recorded.set(sidecar, enumeratedRecord(sidecar, change.provenance, change));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function removeSidecar(options, authority, recorded, path) {
|
|
267
|
+
const sidecar = sidecarPath(path);
|
|
268
|
+
await removeReplicaFile(options.replica, authority.path, sidecar);
|
|
269
|
+
recorded.delete(sidecar);
|
|
270
|
+
}
|
|
271
|
+
|
|
241
272
|
async function applyEnumeration(options, state, enumeration, signal) {
|
|
242
273
|
if (!Array.isArray(enumeration?.authorities)) throw new KitRefusal("kit_sync_rejected", "enumerate");
|
|
243
274
|
for (const view of enumeration.authorities) {
|
|
@@ -245,11 +276,12 @@ async function applyEnumeration(options, state, enumeration, signal) {
|
|
|
245
276
|
if (authority === undefined) throw new KitRefusal("kit_sync_rejected", view.authority);
|
|
246
277
|
const reached = changePosition(view, "enumerate");
|
|
247
278
|
const recorded = new Map(authority.files.map((file) => [file.path, file]));
|
|
248
|
-
for (const change of view.changes ?? []) {
|
|
279
|
+
for (const change of removalsFirst(view.changes ?? [], (change) => change?.removed === true)) {
|
|
249
280
|
if (typeof change?.path !== "string") throw new KitRefusal("kit_sync_rejected", "enumerate");
|
|
250
281
|
if (change.removed === true) {
|
|
251
|
-
await removeReplicaFile(
|
|
282
|
+
await removeReplicaFile(options.replica, authority.path, change.path);
|
|
252
283
|
recorded.delete(change.path);
|
|
284
|
+
await removeSidecar(options, authority, recorded, change.path);
|
|
253
285
|
continue;
|
|
254
286
|
}
|
|
255
287
|
if (typeof change.content === "string") {
|
|
@@ -261,13 +293,7 @@ async function applyEnumeration(options, state, enumeration, signal) {
|
|
|
261
293
|
recorded.set(change.path, enumeratedRecord(change.path, change.content, change));
|
|
262
294
|
}
|
|
263
295
|
if (typeof change.provenance === "string") {
|
|
264
|
-
|
|
265
|
-
await writeReplicaFile(
|
|
266
|
-
locate(options.replica, authority.path, sidecar),
|
|
267
|
-
change.provenance,
|
|
268
|
-
signal,
|
|
269
|
-
);
|
|
270
|
-
recorded.set(sidecar, enumeratedRecord(sidecar, change.provenance, change));
|
|
296
|
+
await writeSidecar(options, authority, recorded, change.path, change, signal);
|
|
271
297
|
}
|
|
272
298
|
}
|
|
273
299
|
authority.files = [...recorded.values()];
|
|
@@ -299,13 +325,41 @@ function uncertainTransport(error) {
|
|
|
299
325
|
);
|
|
300
326
|
}
|
|
301
327
|
|
|
302
|
-
async function
|
|
328
|
+
async function servedProvenance(session, options, state, authority, signal) {
|
|
329
|
+
const enumeration = await replicaDoor(
|
|
330
|
+
session,
|
|
331
|
+
options,
|
|
332
|
+
state,
|
|
333
|
+
KIT_DOOR_ENUMERATE_PATH,
|
|
334
|
+
{
|
|
335
|
+
prefixes: state.prefixes.filter((prefix) => authorityFor(state, prefix) === authority),
|
|
336
|
+
positions: [
|
|
337
|
+
{ authority: authority.authority, position: authority.position, log_epoch: authority.log_epoch },
|
|
338
|
+
],
|
|
339
|
+
},
|
|
340
|
+
signal,
|
|
341
|
+
);
|
|
342
|
+
const view = Array.isArray(enumeration?.authorities)
|
|
343
|
+
? enumeration.authorities.find((entry) => entry?.authority === authority.authority)
|
|
344
|
+
: undefined;
|
|
345
|
+
if (!Array.isArray(view?.changes)) throw new KitRefusal("kit_sync_rejected", "enumerate");
|
|
346
|
+
return new Map(
|
|
347
|
+
view.changes
|
|
348
|
+
.filter((change) => typeof change?.provenance === "string")
|
|
349
|
+
.map((change) => [change.path, change]),
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function acceptBatch(session, options, state, outcome, signal, pending = null) {
|
|
303
354
|
const authority = state.authorities.find((entry) => entry.authority === outcome.authority);
|
|
304
355
|
if (authority === undefined) throw new KitRefusal("kit_sync_rejected", outcome.authority);
|
|
305
356
|
const reached = changePosition(outcome, "batch");
|
|
306
357
|
const recorded = new Map(authority.files.map((file) => [file.path, file]));
|
|
307
358
|
const predicted = predictedReplica(authority, pending);
|
|
308
|
-
|
|
359
|
+
const provenance = (outcome.contents ?? []).some((file) => typeof file?.content === "string")
|
|
360
|
+
? await servedProvenance(session, options, state, authority, signal)
|
|
361
|
+
: new Map();
|
|
362
|
+
for (const file of removalsFirst(outcome.contents ?? [], (file) => file?.content === null)) {
|
|
309
363
|
const relative = relativeTo(authority.path, file.path);
|
|
310
364
|
const destination = locate(options.replica, authority.path, relative);
|
|
311
365
|
const local = await replicaText(destination);
|
|
@@ -319,8 +373,9 @@ async function acceptBatch(options, state, outcome, signal, pending = null) {
|
|
|
319
373
|
local !== accepted &&
|
|
320
374
|
local !== expected;
|
|
321
375
|
if (accepted === null) {
|
|
322
|
-
if (!preserve) await removeReplicaFile(
|
|
376
|
+
if (!preserve) await removeReplicaFile(options.replica, authority.path, relative);
|
|
323
377
|
recorded.delete(relative);
|
|
378
|
+
await removeSidecar(options, authority, recorded, relative);
|
|
324
379
|
continue;
|
|
325
380
|
}
|
|
326
381
|
if (!preserve) await writeReplicaFile(destination, accepted, signal);
|
|
@@ -330,6 +385,10 @@ async function acceptBatch(options, state, outcome, signal, pending = null) {
|
|
|
330
385
|
sha256: "",
|
|
331
386
|
content: accepted,
|
|
332
387
|
});
|
|
388
|
+
const served = provenance.get(relative);
|
|
389
|
+
if (served !== undefined && served.revision === file.revision) {
|
|
390
|
+
await writeSidecar(options, authority, recorded, relative, served, signal);
|
|
391
|
+
}
|
|
333
392
|
}
|
|
334
393
|
authority.files = [...recorded.values()];
|
|
335
394
|
Object.assign(authority, reached);
|
|
@@ -341,16 +400,65 @@ function sameBody(left, right) {
|
|
|
341
400
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
342
401
|
}
|
|
343
402
|
|
|
403
|
+
function contentMap(authority) {
|
|
404
|
+
return Object.fromEntries(authority.files.map((file) => [file.path, file.content]));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async function pullConfirmed(session, options, state, signal) {
|
|
408
|
+
const enumeration = await replicaDoor(
|
|
409
|
+
session,
|
|
410
|
+
options,
|
|
411
|
+
state,
|
|
412
|
+
KIT_DOOR_ENUMERATE_PATH,
|
|
413
|
+
{ prefixes: state.prefixes, positions: positionsOf(state) },
|
|
414
|
+
signal,
|
|
415
|
+
);
|
|
416
|
+
return applyEnumeration(options, state, enumeration, signal);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async function settleExpired(session, options, state, pending, signal) {
|
|
420
|
+
const authority = authorityFor(state, pending.body.changes[0].path);
|
|
421
|
+
const predicted = predictedReplica(authority, pending);
|
|
422
|
+
const before = contentMap(authority);
|
|
423
|
+
await pullConfirmed(session, options, state, signal);
|
|
424
|
+
const confirmed = contentMap(authority);
|
|
425
|
+
const touched = [...new Set([...Object.keys(before), ...Object.keys(predicted)])]
|
|
426
|
+
.filter((path) => before[path] !== predicted[path])
|
|
427
|
+
.sort();
|
|
428
|
+
const landed = touched.length > 0 && touched.every((path) => confirmed[path] === predicted[path]);
|
|
429
|
+
await clearPending(options.directory, state.directory);
|
|
430
|
+
if (!landed) {
|
|
431
|
+
return { pending, accepted: await sendBatch(session, options, state, pending.body.changes, signal) };
|
|
432
|
+
}
|
|
433
|
+
const recorded = new Map(authority.files.map((file) => [file.path, file]));
|
|
434
|
+
return {
|
|
435
|
+
pending,
|
|
436
|
+
accepted: {
|
|
437
|
+
authority: authority.authority,
|
|
438
|
+
position: authority.position,
|
|
439
|
+
log_epoch: authority.log_epoch,
|
|
440
|
+
contents: touched.map((path) => ({
|
|
441
|
+
path: housePath(authority.path, path),
|
|
442
|
+
content: recorded.get(path)?.content ?? null,
|
|
443
|
+
revision: recorded.get(path)?.revision ?? null,
|
|
444
|
+
})),
|
|
445
|
+
},
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
344
449
|
async function recoverPending(session, options, state, signal) {
|
|
345
450
|
const pending = await readPending(options.directory, state.directory);
|
|
346
451
|
if (pending === null) return null;
|
|
347
452
|
try {
|
|
348
453
|
const outcome = await replicaDoor(session, options, state, pending.path, pending.body, signal);
|
|
349
|
-
const accepted = await acceptBatch(options, state, outcome, signal, pending);
|
|
454
|
+
const accepted = await acceptBatch(session, options, state, outcome, signal, pending);
|
|
350
455
|
await clearPending(options.directory, state.directory);
|
|
351
456
|
return { pending, accepted };
|
|
352
457
|
} catch (error) {
|
|
353
458
|
if (uncertainTransport(error)) throw error;
|
|
459
|
+
if (error.code === "operation_expired") {
|
|
460
|
+
return settleExpired(session, options, state, pending, signal);
|
|
461
|
+
}
|
|
354
462
|
await clearPending(options.directory, state.directory);
|
|
355
463
|
throw error;
|
|
356
464
|
}
|
|
@@ -386,7 +494,7 @@ async function sendBatch(session, options, state, changes, signal) {
|
|
|
386
494
|
);
|
|
387
495
|
try {
|
|
388
496
|
const outcome = await replicaDoor(session, options, state, KIT_DOOR_BATCH_PATH, body, signal);
|
|
389
|
-
last = await acceptBatch(options, state, outcome, signal);
|
|
497
|
+
last = await acceptBatch(session, options, state, outcome, signal);
|
|
390
498
|
await clearPending(options.directory, state.directory);
|
|
391
499
|
} catch (error) {
|
|
392
500
|
if (uncertainTransport(error)) throw error;
|
|
@@ -431,16 +539,7 @@ export async function bootstrap(options, dependencies = {}) {
|
|
|
431
539
|
|
|
432
540
|
export async function pull(options, dependencies = {}) {
|
|
433
541
|
return withSession(options, dependencies, async (session, signal) => {
|
|
434
|
-
const
|
|
435
|
-
const enumeration = await replicaDoor(
|
|
436
|
-
session,
|
|
437
|
-
options,
|
|
438
|
-
state,
|
|
439
|
-
KIT_DOOR_ENUMERATE_PATH,
|
|
440
|
-
{ prefixes: state.prefixes, positions: positionsOf(state) },
|
|
441
|
-
signal,
|
|
442
|
-
);
|
|
443
|
-
const next = await applyEnumeration(options, state, enumeration, signal);
|
|
542
|
+
const next = await pullConfirmed(session, options, await loadState(options), signal);
|
|
444
543
|
return { positions: positionsOf(next) };
|
|
445
544
|
});
|
|
446
545
|
}
|
package/src/tasks.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
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 orderedTask(signal, fail, work) {
|
|
28
|
+
let task = null;
|
|
29
|
+
const push = (...args) => {
|
|
30
|
+
const next = (task ?? Promise.resolve())
|
|
31
|
+
.then(() => (signal.aborted ? undefined : work(...args)))
|
|
32
|
+
.catch((error) => {
|
|
33
|
+
if (signal.aborted) return;
|
|
34
|
+
fail(error);
|
|
35
|
+
});
|
|
36
|
+
task = next;
|
|
37
|
+
void next.then(() => {
|
|
38
|
+
if (task === next) task = null;
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
return { push, idle: () => task === null, settled: () => task };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function residentWake() {
|
|
45
|
+
let pending = false;
|
|
46
|
+
let release = null;
|
|
47
|
+
return {
|
|
48
|
+
notify() {
|
|
49
|
+
pending = true;
|
|
50
|
+
release?.();
|
|
51
|
+
},
|
|
52
|
+
async wait(milliseconds, signal) {
|
|
53
|
+
if (!pending && !signal.aborted) {
|
|
54
|
+
const pause = new AbortController();
|
|
55
|
+
const paused = AbortSignal.any([signal, pause.signal]);
|
|
56
|
+
try {
|
|
57
|
+
await Promise.race([
|
|
58
|
+
new Promise((resolve) => {
|
|
59
|
+
release = resolve;
|
|
60
|
+
}),
|
|
61
|
+
Number.isFinite(milliseconds)
|
|
62
|
+
? delay(milliseconds, undefined, { signal: paused })
|
|
63
|
+
: new Promise((resolve) => paused.addEventListener("abort", resolve, { once: true })),
|
|
64
|
+
]);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (!signal.aborted) throw error;
|
|
67
|
+
} finally {
|
|
68
|
+
release = null;
|
|
69
|
+
pause.abort();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const woken = pending;
|
|
73
|
+
pending = false;
|
|
74
|
+
return woken;
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const turns = new Map();
|
|
80
|
+
|
|
81
|
+
export function exclusively(path, work) {
|
|
82
|
+
const turn = (turns.get(path) ?? Promise.resolve()).then(work);
|
|
83
|
+
const settled = turn.then(
|
|
84
|
+
() => undefined,
|
|
85
|
+
() => undefined,
|
|
86
|
+
);
|
|
87
|
+
turns.set(path, settled);
|
|
88
|
+
void settled.then(() => {
|
|
89
|
+
if (turns.get(path) === settled) turns.delete(path);
|
|
90
|
+
});
|
|
91
|
+
return turn;
|
|
92
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
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
|
+
export function slices(text, limit = TELEGRAM_RICH_MESSAGE_CHARACTERS_MAX) {
|
|
29
|
+
const pieces = [];
|
|
30
|
+
let start = 0;
|
|
31
|
+
while (start < text.length) {
|
|
32
|
+
let end = Math.min(start + limit, text.length);
|
|
33
|
+
const window = text.slice(start, end);
|
|
34
|
+
const line = window.lastIndexOf("\n");
|
|
35
|
+
const last = text.charCodeAt(end - 1);
|
|
36
|
+
if (end < text.length && line > 0 && /\S/.test(window.slice(0, line))) end = start + line + 1;
|
|
37
|
+
else if (end < text.length && last >= 0xd800 && last <= 0xdbff) end -= 1;
|
|
38
|
+
pieces.push(text.slice(start, end));
|
|
39
|
+
start = end;
|
|
40
|
+
}
|
|
41
|
+
return pieces;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function grouped(block, listed) {
|
|
45
|
+
const groups = [];
|
|
46
|
+
let group = [];
|
|
47
|
+
let characters = 0;
|
|
48
|
+
for (const entry of block[listed]) {
|
|
49
|
+
const length = richCharacters(entry);
|
|
50
|
+
if (group.length > 0 && characters + length > TELEGRAM_RICH_MESSAGE_CHARACTERS_MAX) {
|
|
51
|
+
groups.push(group);
|
|
52
|
+
group = [];
|
|
53
|
+
characters = 0;
|
|
54
|
+
}
|
|
55
|
+
group.push(entry);
|
|
56
|
+
characters += length;
|
|
57
|
+
}
|
|
58
|
+
if (group.length > 0) groups.push(group);
|
|
59
|
+
return groups.flatMap((entries) => fitted({ ...block, [listed]: entries }));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function fitted(block) {
|
|
63
|
+
if (richCharacters(block) <= TELEGRAM_RICH_MESSAGE_CHARACTERS_MAX) return [block];
|
|
64
|
+
if (typeof block.text === "string") return slices(block.text).map((text) => ({ ...block, text }));
|
|
65
|
+
if (typeof block.expression === "string") {
|
|
66
|
+
return slices(block.expression).map((expression) => ({ ...block, expression }));
|
|
67
|
+
}
|
|
68
|
+
for (const listed of ["items", "cells"]) {
|
|
69
|
+
if (Array.isArray(block[listed]) && block[listed].length > 1) return grouped(block, listed);
|
|
70
|
+
}
|
|
71
|
+
return slices(richText(block).join("\n")).map((text) => ({ type: "paragraph", text }));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function richPieces(message) {
|
|
75
|
+
const pieces = [];
|
|
76
|
+
let blocks = [];
|
|
77
|
+
let characters = 0;
|
|
78
|
+
for (const part of message.blocks.flatMap(fitted)) {
|
|
79
|
+
const length = richCharacters(part);
|
|
80
|
+
if (
|
|
81
|
+
blocks.length > 0 &&
|
|
82
|
+
(blocks.length >= TELEGRAM_RICH_MESSAGE_BLOCKS_MAX ||
|
|
83
|
+
characters + length > TELEGRAM_RICH_MESSAGE_CHARACTERS_MAX)
|
|
84
|
+
) {
|
|
85
|
+
pieces.push({ ...message, blocks });
|
|
86
|
+
blocks = [];
|
|
87
|
+
characters = 0;
|
|
88
|
+
}
|
|
89
|
+
blocks.push(part);
|
|
90
|
+
characters += length;
|
|
91
|
+
}
|
|
92
|
+
pieces.push({ ...message, blocks });
|
|
93
|
+
return pieces;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function listItems(items, ordered) {
|
|
97
|
+
return items.map((item, index) => ({
|
|
98
|
+
blocks: [{ type: "paragraph", text: item }],
|
|
99
|
+
...(ordered ? { value: index + 1, type: "1" } : {}),
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function telegramBlock(block) {
|
|
104
|
+
if (typeof block !== "object" || block === null) return null;
|
|
105
|
+
if (block.type === "paragraph") return { type: "paragraph", text: block.text };
|
|
106
|
+
if (block.type === "heading") return { type: "heading", size: block.level, text: block.text };
|
|
107
|
+
if (block.type === "code") {
|
|
108
|
+
return {
|
|
109
|
+
type: "pre",
|
|
110
|
+
...(typeof block.language === "string" ? { language: block.language } : {}),
|
|
111
|
+
text: block.text,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
if (block.type === "list" && Array.isArray(block.items)) {
|
|
115
|
+
return { type: "list", items: listItems(block.items, block.ordered === true) };
|
|
116
|
+
}
|
|
117
|
+
if (block.type === "table" && Array.isArray(block.headers) && Array.isArray(block.rows)) {
|
|
118
|
+
return {
|
|
119
|
+
type: "table",
|
|
120
|
+
cells: [
|
|
121
|
+
block.headers.map((text) => ({ text, is_header: true })),
|
|
122
|
+
...block.rows.map((row) => (Array.isArray(row) ? row : []).map((text) => ({ text }))),
|
|
123
|
+
],
|
|
124
|
+
is_bordered: true,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (block.type === "formula") return { type: "mathematical_expression", expression: block.expression };
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function planBlocks(steps) {
|
|
132
|
+
return [
|
|
133
|
+
{ type: "heading", text: "Plan", size: 3 },
|
|
134
|
+
{
|
|
135
|
+
type: "list",
|
|
136
|
+
items: steps.map((step, index) => ({
|
|
137
|
+
blocks: [{ type: "paragraph", text: [`${step.label} — `, { type: "bold", text: step.status }] }],
|
|
138
|
+
value: index + 1,
|
|
139
|
+
type: "1",
|
|
140
|
+
})),
|
|
141
|
+
},
|
|
142
|
+
];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function draftId(turn) {
|
|
146
|
+
return (createHash("sha256").update(turn).digest().readUInt32BE(0) % 2_147_483_647) + 1;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function viewFrame(frame) {
|
|
150
|
+
if (typeof frame !== "object" || frame === null || !Object.hasOwn(PARTS, frame.type)) return null;
|
|
151
|
+
for (const named of ["conversation_id", "turn_id"]) {
|
|
152
|
+
if (typeof frame[named] !== "string" || frame[named] === "") return null;
|
|
153
|
+
}
|
|
154
|
+
if (!Number.isSafeInteger(frame.sequence) || frame.sequence < 1) return null;
|
|
155
|
+
if (!Number.isSafeInteger(frame.from) || frame.from < 0) return null;
|
|
156
|
+
return Array.isArray(frame[PARTS[frame.type]]) ? frame : null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function turnViews(options) {
|
|
160
|
+
let threads = new Map();
|
|
161
|
+
const views = new Map();
|
|
162
|
+
|
|
163
|
+
function applied(view, frame) {
|
|
164
|
+
const items = frame[PARTS[frame.type]];
|
|
165
|
+
const held = view[frame.type];
|
|
166
|
+
if (held !== null && frame.sequence <= held.sequence) return false;
|
|
167
|
+
if (frame.from === 0) {
|
|
168
|
+
view[frame.type] = { sequence: frame.sequence, items };
|
|
169
|
+
view.asked = false;
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
if (held === null || frame.sequence !== held.sequence + 1 || frame.from > held.items.length) {
|
|
173
|
+
if (!view.asked) {
|
|
174
|
+
view.asked = true;
|
|
175
|
+
options.gap({ type: "gap", conversation_id: frame.conversation_id, turn_id: frame.turn_id });
|
|
176
|
+
}
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
view[frame.type] = { sequence: frame.sequence, items: [...held.items.slice(0, frame.from), ...items] };
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
address(listed) {
|
|
185
|
+
threads = new Map(listed.map((thread) => [thread.conversation_id, thread]));
|
|
186
|
+
for (const conversation of views.keys()) {
|
|
187
|
+
if (!threads.has(conversation)) views.delete(conversation);
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
receive(value) {
|
|
191
|
+
const frame = viewFrame(value);
|
|
192
|
+
if (frame === null || !threads.has(frame.conversation_id)) return null;
|
|
193
|
+
let view = views.get(frame.conversation_id);
|
|
194
|
+
if (view === undefined || view.turn !== frame.turn_id) {
|
|
195
|
+
view = { turn: frame.turn_id, draft: null, plan: null, asked: false };
|
|
196
|
+
views.set(frame.conversation_id, view);
|
|
197
|
+
}
|
|
198
|
+
return applied(view, frame) ? frame.conversation_id : null;
|
|
199
|
+
},
|
|
200
|
+
draft(conversation) {
|
|
201
|
+
const view = views.get(conversation);
|
|
202
|
+
const thread = threads.get(conversation);
|
|
203
|
+
if (view === undefined || thread === undefined) return null;
|
|
204
|
+
const blocks = [
|
|
205
|
+
...(view.plan === null ? [] : planBlocks(view.plan.items)),
|
|
206
|
+
...(view.draft === null ? [] : view.draft.items.map(telegramBlock).filter((block) => block !== null)),
|
|
207
|
+
];
|
|
208
|
+
if (blocks.length === 0) return null;
|
|
209
|
+
return {
|
|
210
|
+
chat_id: thread.chat_id,
|
|
211
|
+
topic_id: thread.topic_id,
|
|
212
|
+
draft_id: draftId(view.turn),
|
|
213
|
+
rich_message: richPieces({ blocks })[0],
|
|
214
|
+
};
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|