@agentshouse/kit 0.1.0-alpha.33 → 0.1.0-alpha.34
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 +21 -10
- package/bootstrap.json +3 -3
- package/package.json +1 -1
- package/src/contract.js +2 -0
- package/src/google/entries.js +4 -1
- package/src/sync.js +26 -10
- package/src/telegram-view.js +6 -3
- package/src/telegram.js +44 -4
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ The Skill set Kit installs is Matt Pocock's
|
|
|
12
12
|
On Linux `amd64` or `arm64`, run the published, versioned bootstrap once:
|
|
13
13
|
|
|
14
14
|
```sh
|
|
15
|
-
bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
15
|
+
bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.34/connect-linux.sh)
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
The bootstrap first establishes Docker Engine from what it observes on the
|
|
@@ -445,7 +445,8 @@ service to a signed-in account asks only for that service's consent, and each
|
|
|
445
445
|
service keeps its own grant. An address that answers another consent, or a code
|
|
446
446
|
Google refuses to exchange, stores nothing; a declined consent or a ceremony
|
|
447
447
|
that outlives its ten-minute bound cancels the sign-in, and the Kit tries again
|
|
448
|
-
at the next reconciliation.
|
|
448
|
+
at the next reconciliation. A capture that finds the grant revoked removes it,
|
|
449
|
+
reports the entry `signed_out` and opens a new sign-in at once.
|
|
449
450
|
|
|
450
451
|
The resident captures each signed-in entry every 15 minutes, with no command.
|
|
451
452
|
An occurrence never overlaps its entry's previous one, and one missed while the
|
|
@@ -1576,8 +1577,9 @@ kit sync rebootstrap ./replica
|
|
|
1576
1577
|
```
|
|
1577
1578
|
|
|
1578
1579
|
`bootstrap` writes `ROOM.md`, Library text, House-derived projections, and
|
|
1579
|
-
Capture text with
|
|
1580
|
-
|
|
1580
|
+
Capture text, with the provenance House serves on a file's manifest entry as
|
|
1581
|
+
its `_provenance` sidecar, and records one position per selected authority from
|
|
1582
|
+
the bundle, with the log epoch House issued it in, together with
|
|
1581
1583
|
the positive operation scopes House declared for it: `read`, `sync`, `write`,
|
|
1582
1584
|
and `delete`, each a list of paths relative to the authority under which that
|
|
1583
1585
|
operation is admitted. Managed replication needs `read` plus `sync` on a Room;
|
|
@@ -1586,11 +1588,11 @@ with `operation_denied`. Generated `_provenance` files delivered beside authored
|
|
|
1586
1588
|
Library paths are House metadata, not authored Library documents. `pull` presents
|
|
1587
1589
|
each recorded position with its log epoch, applies each enumerated path's whole
|
|
1588
1590
|
content or removal, and advances those positions. A removal, pulled or accepted
|
|
1589
|
-
from `submit`, also removes
|
|
1590
|
-
so a later file can take that path; a
|
|
1591
|
-
local draft, stays. A position older
|
|
1592
|
-
declared retention, or one of a log epoch before House's database
|
|
1593
|
-
is reported as `position_expired`; only `rebootstrap` rebuilds the directory.
|
|
1591
|
+
from `submit`, also removes the file's `_provenance` sidecar and each directory
|
|
1592
|
+
it leaves empty up to the authority, so a later file can take that path; a
|
|
1593
|
+
directory still holding a file, such as a local draft, stays. A position older
|
|
1594
|
+
than House's declared retention, or one of a log epoch before House's database
|
|
1595
|
+
was restored, is reported as `position_expired`; only `rebootstrap` rebuilds the directory.
|
|
1594
1596
|
|
|
1595
1597
|
`submit` sends field, section, and preamble operations with that value's prior
|
|
1596
1598
|
content as base, and whole-file, rename, and removal operations with the file
|
|
@@ -1803,7 +1805,12 @@ another, beside the live drafts described below:
|
|
|
1803
1805
|
the answer is absent or unreadable. File items are pulled back from House,
|
|
1804
1806
|
refused as `file_too_large` above the 10 MiB image ceiling, uploaded as
|
|
1805
1807
|
`sendPhoto` with the caption House composed around the link to the retained
|
|
1806
|
-
original, and reported the same way
|
|
1808
|
+
original, and reported the same way. A caption past Telegram's
|
|
1809
|
+
1,024-character caption limit keeps its head under the image, cut at its last
|
|
1810
|
+
line break that fits or else at the limit, and the rest follows in the topic
|
|
1811
|
+
as ordinary messages; the upload is reported with the image's answer when
|
|
1812
|
+
every part was delivered and otherwise with the answer of the first part that
|
|
1813
|
+
was not. A pull that fails reports
|
|
1807
1814
|
`transfer_failed` and an ambiguous upload answer reports `delivery_unknown`.
|
|
1808
1815
|
Only an image ever becomes a file item, because House offers every other file
|
|
1809
1816
|
as a `conversation_file_link` message this queue delivers like any other. The
|
|
@@ -1814,6 +1821,10 @@ another, beside the live drafts described below:
|
|
|
1814
1821
|
between blocks and inside a block only when that block alone exceeds them,
|
|
1815
1822
|
and reported once, `delivered` with the first message's id when every part
|
|
1816
1823
|
was delivered and otherwise with the answer of the first part that was not.
|
|
1824
|
+
A new plain-text message past Telegram's 4,096-character message limit, such
|
|
1825
|
+
as a `conversation_file_link` text, is sent and reported the same way, cut
|
|
1826
|
+
at the last line break that fits or else at the limit, so the first message
|
|
1827
|
+
keeps its reply and keyboard and a link on its own line stays whole.
|
|
1817
1828
|
Each answer also names the `threads` the bot's topics carry, a
|
|
1818
1829
|
`conversation_id` with its `chat_id` and `topic_id`, which is where live
|
|
1819
1830
|
drafts go.
|
package/bootstrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.34",
|
|
4
4
|
"hosts": {
|
|
5
5
|
"linux": {
|
|
6
|
-
"command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
6
|
+
"command": "bash <(curl -fsSL https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.34/connect-linux.sh)",
|
|
7
7
|
"asset": {
|
|
8
8
|
"name": "connect-linux.sh",
|
|
9
|
-
"url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.
|
|
9
|
+
"url": "https://github.com/agentshouse/kit/releases/download/v0.1.0-alpha.34/connect-linux.sh"
|
|
10
10
|
},
|
|
11
11
|
"workspace": {
|
|
12
12
|
"argument": "--workspace",
|
package/package.json
CHANGED
package/src/contract.js
CHANGED
|
@@ -61,6 +61,8 @@ export const TELEGRAM_DOWNLOAD_CEILING_BYTES = 20 * 1024 * 1024;
|
|
|
61
61
|
export const TELEGRAM_BOT_API_TIMEOUT_MS = 30_000;
|
|
62
62
|
export const TELEGRAM_GATEWAY_POLL_MS = 2_000;
|
|
63
63
|
export const TELEGRAM_RICH_MESSAGE_CHARACTERS_MAX = 32_768;
|
|
64
|
+
export const TELEGRAM_MESSAGE_CHARACTERS_MAX = 4_096;
|
|
65
|
+
export const TELEGRAM_CAPTION_CHARACTERS_MAX = 1_024;
|
|
64
66
|
export const TELEGRAM_RICH_MESSAGE_BLOCKS_MAX = 500;
|
|
65
67
|
export const TELEGRAM_CONTROL_KEYBOARD_COMMANDS = ["/stop", "/kill"];
|
|
66
68
|
export const TELEGRAM_MEDIA_KINDS = [
|
package/src/google/entries.js
CHANGED
|
@@ -213,7 +213,10 @@ export function sourceEntries(context) {
|
|
|
213
213
|
const task = signIn(occasion(record), entry);
|
|
214
214
|
signingIn = { source: entry.source, task };
|
|
215
215
|
try {
|
|
216
|
-
if (await task)
|
|
216
|
+
if (await task) {
|
|
217
|
+
record.last = null;
|
|
218
|
+
attempted.delete(entry.source);
|
|
219
|
+
}
|
|
217
220
|
} catch (error) {
|
|
218
221
|
if (!record.stop.signal.aborted) throw error;
|
|
219
222
|
} finally {
|
package/src/sync.js
CHANGED
|
@@ -131,23 +131,31 @@ function scopeContents(replica, scope) {
|
|
|
131
131
|
return scope.contents;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
function servedFile(scope, path) {
|
|
135
|
+
return scope.manifest.files.find((file) => file?.path === path);
|
|
136
|
+
}
|
|
137
|
+
|
|
134
138
|
async function writeScope(replica, scope, signal) {
|
|
135
|
-
const
|
|
139
|
+
const recorded = new Map();
|
|
136
140
|
for (const entry of scopeContents(replica, scope)) {
|
|
137
|
-
const meta = scope
|
|
141
|
+
const meta = servedFile(scope, entry.path) ?? {};
|
|
138
142
|
await writeReplicaFile(locate(replica, scope.manifest.path, entry.path), entry.content, signal);
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
content: entry.content,
|
|
144
|
-
});
|
|
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
|
+
}
|
|
145
147
|
}
|
|
146
|
-
return
|
|
148
|
+
return [...recorded.values()];
|
|
147
149
|
}
|
|
148
150
|
|
|
149
151
|
async function replaceScope(replica, previous, scope, signal) {
|
|
150
|
-
const kept = new Set(
|
|
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
|
+
);
|
|
151
159
|
const root = locate(replica, scope.manifest.path);
|
|
152
160
|
for (const path of await listFiles(root)) {
|
|
153
161
|
if (!kept.has(path)) await removeReplicaFile(replica, scope.manifest.path, path);
|
|
@@ -255,6 +263,12 @@ async function writeSidecar(options, authority, recorded, path, change, signal)
|
|
|
255
263
|
recorded.set(sidecar, enumeratedRecord(sidecar, change.provenance, change));
|
|
256
264
|
}
|
|
257
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
|
+
|
|
258
272
|
async function applyEnumeration(options, state, enumeration, signal) {
|
|
259
273
|
if (!Array.isArray(enumeration?.authorities)) throw new KitRefusal("kit_sync_rejected", "enumerate");
|
|
260
274
|
for (const view of enumeration.authorities) {
|
|
@@ -267,6 +281,7 @@ async function applyEnumeration(options, state, enumeration, signal) {
|
|
|
267
281
|
if (change.removed === true) {
|
|
268
282
|
await removeReplicaFile(options.replica, authority.path, change.path);
|
|
269
283
|
recorded.delete(change.path);
|
|
284
|
+
await removeSidecar(options, authority, recorded, change.path);
|
|
270
285
|
continue;
|
|
271
286
|
}
|
|
272
287
|
if (typeof change.content === "string") {
|
|
@@ -360,6 +375,7 @@ async function acceptBatch(session, options, state, outcome, signal, pending = n
|
|
|
360
375
|
if (accepted === null) {
|
|
361
376
|
if (!preserve) await removeReplicaFile(options.replica, authority.path, relative);
|
|
362
377
|
recorded.delete(relative);
|
|
378
|
+
await removeSidecar(options, authority, recorded, relative);
|
|
363
379
|
continue;
|
|
364
380
|
}
|
|
365
381
|
if (!preserve) await writeReplicaFile(destination, accepted, signal);
|
package/src/telegram-view.js
CHANGED
|
@@ -25,13 +25,16 @@ function richText(value, key = null) {
|
|
|
25
25
|
return [];
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
function slices(text) {
|
|
28
|
+
export function slices(text, limit = TELEGRAM_RICH_MESSAGE_CHARACTERS_MAX) {
|
|
29
29
|
const pieces = [];
|
|
30
30
|
let start = 0;
|
|
31
31
|
while (start < text.length) {
|
|
32
|
-
let end = Math.min(start +
|
|
32
|
+
let end = Math.min(start + limit, text.length);
|
|
33
|
+
const window = text.slice(start, end);
|
|
34
|
+
const line = window.lastIndexOf("\n");
|
|
33
35
|
const last = text.charCodeAt(end - 1);
|
|
34
|
-
if (end < text.length &&
|
|
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;
|
|
35
38
|
pieces.push(text.slice(start, end));
|
|
36
39
|
start = end;
|
|
37
40
|
}
|
package/src/telegram.js
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
PERSONAL_BOT_OFFSET_RELATIVE_PATH,
|
|
11
11
|
PERSONAL_BOT_TOKEN_RELATIVE_PATH,
|
|
12
12
|
TELEGRAM_BOT_API_TIMEOUT_MS,
|
|
13
|
+
TELEGRAM_CAPTION_CHARACTERS_MAX,
|
|
13
14
|
TELEGRAM_CONTROL_KEYBOARD_COMMANDS,
|
|
14
15
|
TELEGRAM_DELIVERY_BATCH_MAX,
|
|
15
16
|
TELEGRAM_DELIVERY_KINDS,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
TELEGRAM_IMAGE_UPLOAD_CEILING_BYTES,
|
|
19
20
|
TELEGRAM_INGRESS_BATCH_MAX,
|
|
20
21
|
TELEGRAM_MEDIA_KINDS,
|
|
22
|
+
TELEGRAM_MESSAGE_CHARACTERS_MAX,
|
|
21
23
|
TELEGRAM_UNSUPPORTED_MEDIA_FIELDS,
|
|
22
24
|
TELEGRAM_UPDATE_OUTCOMES,
|
|
23
25
|
telegramApiUrl,
|
|
@@ -27,7 +29,7 @@ import { heldControl, houseRequest, maintainControl, openSession } from "./house
|
|
|
27
29
|
import { watchKitWork } from "./kit-work-stream.js";
|
|
28
30
|
import { KitRefusal } from "./refusal.js";
|
|
29
31
|
import { writeText } from "./state.js";
|
|
30
|
-
import { richPieces, turnViews } from "./telegram-view.js";
|
|
32
|
+
import { richPieces, slices, turnViews } from "./telegram-view.js";
|
|
31
33
|
import { kitStreamWriter } from "./turn-frames.js";
|
|
32
34
|
|
|
33
35
|
const TOKEN_PROMPT = "Paste the BotFather token for this Environment: ";
|
|
@@ -285,8 +287,21 @@ export function deliveryCall(delivery) {
|
|
|
285
287
|
return { method: "sendMessage", body: sendBody(delivery) };
|
|
286
288
|
}
|
|
287
289
|
|
|
290
|
+
function topicMessage(chat, topic, text) {
|
|
291
|
+
const body = { chat_id: Number(chat), text };
|
|
292
|
+
if (topic !== null) body.message_thread_id = Number(topic);
|
|
293
|
+
return { method: "sendMessage", body };
|
|
294
|
+
}
|
|
295
|
+
|
|
288
296
|
function deliveryCalls(delivery) {
|
|
289
297
|
const call = deliveryCall(delivery);
|
|
298
|
+
if (call.method === "sendMessage" && call.body.text.length > TELEGRAM_MESSAGE_CHARACTERS_MAX) {
|
|
299
|
+
const [first, ...following] = slices(call.body.text, TELEGRAM_MESSAGE_CHARACTERS_MAX);
|
|
300
|
+
return [
|
|
301
|
+
{ method: call.method, body: { ...call.body, text: first } },
|
|
302
|
+
...following.map((text) => topicMessage(delivery.chat_id, delivery.topic_id, text)),
|
|
303
|
+
];
|
|
304
|
+
}
|
|
290
305
|
if (call.method !== "sendRichMessage") return [call];
|
|
291
306
|
return richPieces(call.body.rich_message).map((rich_message) => ({
|
|
292
307
|
method: call.method,
|
|
@@ -791,6 +806,27 @@ async function sendQueuedMessage(gateway, token, delivery) {
|
|
|
791
806
|
return false;
|
|
792
807
|
}
|
|
793
808
|
|
|
809
|
+
function captionPieces(caption) {
|
|
810
|
+
if (caption === null || caption.length <= TELEGRAM_CAPTION_CHARACTERS_MAX) return [caption];
|
|
811
|
+
const [head] = slices(caption, TELEGRAM_CAPTION_CHARACTERS_MAX);
|
|
812
|
+
return [head, ...slices(caption.slice(head.length), TELEGRAM_MESSAGE_CHARACTERS_MAX)];
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
async function sentOverflow(gateway, token, upload, overflow) {
|
|
816
|
+
for (const text of overflow) {
|
|
817
|
+
const call = topicMessage(upload.chat_id, upload.topic_id, text);
|
|
818
|
+
let answered;
|
|
819
|
+
try {
|
|
820
|
+
answered = await telegramPost(gateway, telegramApiUrl(token, call.method), call.body);
|
|
821
|
+
} catch (error) {
|
|
822
|
+
if (cancelled(error)) throw error;
|
|
823
|
+
return { settled: uploadOutcome(null), answered: null };
|
|
824
|
+
}
|
|
825
|
+
if (!botApiAccepted(answered)) return { settled: uploadOutcome(answered), answered };
|
|
826
|
+
}
|
|
827
|
+
return null;
|
|
828
|
+
}
|
|
829
|
+
|
|
794
830
|
async function sendQueuedUpload(gateway, token, upload) {
|
|
795
831
|
let bytes;
|
|
796
832
|
try {
|
|
@@ -810,12 +846,13 @@ async function sendQueuedUpload(gateway, token, upload) {
|
|
|
810
846
|
});
|
|
811
847
|
return false;
|
|
812
848
|
}
|
|
849
|
+
const [caption, ...overflow] = captionPieces(upload.caption);
|
|
813
850
|
let answered;
|
|
814
851
|
try {
|
|
815
852
|
answered = await telegramUpload(
|
|
816
853
|
gateway,
|
|
817
854
|
telegramApiUrl(token, "sendPhoto"),
|
|
818
|
-
uploadFields(upload),
|
|
855
|
+
uploadFields({ ...upload, caption }),
|
|
819
856
|
{ field: "photo", name: upload.file_name, bytes },
|
|
820
857
|
);
|
|
821
858
|
} catch (error) {
|
|
@@ -826,8 +863,11 @@ async function sendQueuedUpload(gateway, token, upload) {
|
|
|
826
863
|
});
|
|
827
864
|
return false;
|
|
828
865
|
}
|
|
829
|
-
|
|
830
|
-
const
|
|
866
|
+
const photographed = uploadOutcome(answered);
|
|
867
|
+
const unsent =
|
|
868
|
+
photographed.outcome === "delivered" ? await sentOverflow(gateway, token, upload, overflow) : null;
|
|
869
|
+
await reportUpload(gateway, upload, unsent?.settled ?? photographed);
|
|
870
|
+
const retryAfter = telegramRetryAfter(unsent === null ? answered : unsent.answered);
|
|
831
871
|
if (retryAfter === null) return false;
|
|
832
872
|
gateway.gate.hold(retryAfter);
|
|
833
873
|
return true;
|