@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 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.33/connect-linux.sh)
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 its provenance sidecar, and records one position per selected
1580
- authority from the bundle, with the log epoch House issued it in, together with
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 each directory it leaves empty up to the authority,
1590
- so a later file can take that path; a directory still holding a file, such as a
1591
- local draft, stays. A position older than House's
1592
- declared retention, or one of a log epoch before House's database was restored,
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; a pull that fails reports
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.33",
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.33/connect-linux.sh)",
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.33/connect-linux.sh"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentshouse/kit",
3
- "version": "0.1.0-alpha.33",
3
+ "version": "0.1.0-alpha.34",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
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 = [
@@ -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) record.last = null;
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 files = [];
139
+ const recorded = new Map();
136
140
  for (const entry of scopeContents(replica, scope)) {
137
- const meta = scope.manifest.files.find((file) => file.path === entry.path);
141
+ const meta = servedFile(scope, entry.path) ?? {};
138
142
  await writeReplicaFile(locate(replica, scope.manifest.path, entry.path), entry.content, signal);
139
- files.push({
140
- path: entry.path,
141
- revision: typeof meta?.revision === "string" ? meta.revision : "",
142
- sha256: typeof meta?.sha256 === "string" ? meta.sha256 : "",
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 files;
148
+ return [...recorded.values()];
147
149
  }
148
150
 
149
151
  async function replaceScope(replica, previous, scope, signal) {
150
- const kept = new Set(scopeContents(replica, scope).map((entry) => entry.path));
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);
@@ -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 + TELEGRAM_RICH_MESSAGE_CHARACTERS_MAX, text.length);
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 && last >= 0xd800 && last <= 0xdbff) 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;
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
- await reportUpload(gateway, upload, uploadOutcome(answered));
830
- const retryAfter = telegramRetryAfter(answered);
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;