@rubytech/create-maxy-code 0.1.575 → 0.1.577

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.
Files changed (83) hide show
  1. package/dist/__tests__/preserve-house-cloudflare.test.js +2 -72
  2. package/dist/index.js +8 -59
  3. package/dist/preserve-house-cloudflare.js +2 -49
  4. package/package.json +1 -1
  5. package/payload/platform/lib/active-rules/dist/index.d.ts.map +1 -1
  6. package/payload/platform/lib/active-rules/dist/index.js +32 -1
  7. package/payload/platform/lib/active-rules/dist/index.js.map +1 -1
  8. package/payload/platform/lib/active-rules/src/index.test.ts +52 -3
  9. package/payload/platform/lib/active-rules/src/index.ts +36 -2
  10. package/payload/platform/lib/dispatch-read/dist/allocate.d.ts +57 -0
  11. package/payload/platform/lib/dispatch-read/dist/allocate.d.ts.map +1 -0
  12. package/payload/platform/lib/dispatch-read/dist/allocate.js +33 -0
  13. package/payload/platform/lib/dispatch-read/dist/allocate.js.map +1 -0
  14. package/payload/platform/lib/dispatch-read/dist/index.d.ts +2 -0
  15. package/payload/platform/lib/dispatch-read/dist/index.d.ts.map +1 -1
  16. package/payload/platform/lib/dispatch-read/dist/index.js +10 -1
  17. package/payload/platform/lib/dispatch-read/dist/index.js.map +1 -1
  18. package/payload/platform/lib/dispatch-read/src/__tests__/allocate.test.ts +41 -0
  19. package/payload/platform/lib/dispatch-read/src/allocate.ts +89 -0
  20. package/payload/platform/lib/dispatch-read/src/index.ts +14 -0
  21. package/payload/platform/plugins/admin/hooks/__tests__/prompt-optimiser-compliance.test.sh +7 -2
  22. package/payload/platform/plugins/admin/hooks/prompt-optimiser-compliance.sh +8 -2
  23. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +2 -2
  24. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +12 -0
  25. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-visit-status-write.test.ts +333 -22
  26. package/payload/platform/plugins/cloudflare/mcp/__tests__/portal-visits-scope.test.ts +262 -0
  27. package/payload/platform/plugins/cloudflare/skills/data-portal/schema.sql +126 -21
  28. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/visit-status.ts +142 -2
  29. package/payload/platform/plugins/cloudflare/skills/data-portal/template/functions/api/visits.ts +118 -6
  30. package/payload/platform/plugins/dispatch/PLUGIN.md +2 -0
  31. package/payload/platform/plugins/dispatch/mcp/dist/lib/allocate.d.ts +19 -50
  32. package/payload/platform/plugins/dispatch/mcp/dist/lib/allocate.d.ts.map +1 -1
  33. package/payload/platform/plugins/dispatch/mcp/dist/lib/allocate.js +1 -28
  34. package/payload/platform/plugins/dispatch/mcp/dist/lib/allocate.js.map +1 -1
  35. package/payload/platform/plugins/dispatch/skills/dispatch-page/SKILL.md +45 -6
  36. package/payload/platform/plugins/dispatch/skills/dispatch-page/template/planner.css +60 -0
  37. package/payload/platform/plugins/dispatch/skills/dispatch-page/template/planner.html +13 -0
  38. package/payload/platform/plugins/dispatch/skills/dispatch-page/template/planner.js +203 -5
  39. package/payload/platform/plugins/docs/references/internals.md +1 -1
  40. package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-path.test.d.ts +2 -0
  41. package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-path.test.d.ts.map +1 -0
  42. package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-path.test.js +19 -0
  43. package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-path.test.js.map +1 -0
  44. package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-register.test.d.ts +2 -0
  45. package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-register.test.d.ts.map +1 -0
  46. package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-register.test.js +110 -0
  47. package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-register.test.js.map +1 -0
  48. package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-verdict.test.d.ts +2 -0
  49. package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-verdict.test.d.ts.map +1 -0
  50. package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-verdict.test.js +22 -0
  51. package/payload/platform/plugins/telegram/mcp/dist/__tests__/webhook-verdict.test.js.map +1 -0
  52. package/payload/platform/plugins/telegram/mcp/dist/index.js +19 -163
  53. package/payload/platform/plugins/telegram/mcp/dist/index.js.map +1 -1
  54. package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.d.ts +9 -0
  55. package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.d.ts.map +1 -1
  56. package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.js +11 -0
  57. package/payload/platform/plugins/telegram/mcp/dist/lib/account-write.js.map +1 -1
  58. package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.d.ts +30 -3
  59. package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.d.ts.map +1 -1
  60. package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.js +29 -4
  61. package/payload/platform/plugins/telegram/mcp/dist/lib/telegram.js.map +1 -1
  62. package/payload/platform/plugins/telegram/mcp/dist/tools/webhook-register.d.ts +33 -0
  63. package/payload/platform/plugins/telegram/mcp/dist/tools/webhook-register.d.ts.map +1 -0
  64. package/payload/platform/plugins/telegram/mcp/dist/tools/webhook-register.js +145 -0
  65. package/payload/platform/plugins/telegram/mcp/dist/tools/webhook-register.js.map +1 -0
  66. package/payload/platform/plugins/telegram/skills/configure/SKILL.md +4 -2
  67. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts +5 -0
  68. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  69. package/payload/platform/services/claude-session-manager/dist/http-server.js +29 -0
  70. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  71. package/payload/platform/services/telegram-channel/dist/notification.d.ts +35 -3
  72. package/payload/platform/services/telegram-channel/dist/notification.d.ts.map +1 -1
  73. package/payload/platform/services/telegram-channel/dist/notification.js +38 -5
  74. package/payload/platform/services/telegram-channel/dist/notification.js.map +1 -1
  75. package/payload/platform/services/telegram-channel/dist/server.js +1 -1
  76. package/payload/platform/services/telegram-channel/dist/server.js.map +1 -1
  77. package/payload/platform/services/webchat-channel/dist/notification.d.ts +2 -1
  78. package/payload/platform/services/webchat-channel/dist/notification.d.ts.map +1 -1
  79. package/payload/platform/services/webchat-channel/dist/notification.js.map +1 -1
  80. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +2 -2
  81. package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
  82. package/payload/server/server.js +670 -155
  83. package/dist/__tests__/converge-client-admins.test.js +0 -50
@@ -114,20 +114,133 @@
114
114
  column order above. */
115
115
  var HOUR_KEYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'];
116
116
 
117
- /* One visit. Clicking moves it to the next declared status and posts that
118
- through the portal's existing write door. Which transitions are legal is
119
- Task 2511's; this cycles the declared list. */
120
- function visitButton(v, meta, zone) {
117
+ /* Whether the account declared this move (Task 2511).
118
+
119
+ Absent from the published list means refused, and the list travels with the
120
+ week, so a drag is refused with the install offline. `visit-status.ts`
121
+ refuses it a second time from the same list; that refusal is the binding
122
+ one, and this one exists so the operator is told now rather than in sixty
123
+ seconds. An unchanged status is NOT a move, which is the same rule the door
124
+ applies: `[from, to]` declares a move and no account declares a
125
+ self-transition, so without this every pure re-timing would be refused. */
126
+ function declared(meta, from, to) {
127
+ if (from === to) return true;
128
+ var list = (meta && meta.transitions) || [];
129
+ for (var i = 0; i < list.length; i++) {
130
+ if (list[i][0] === from && list[i][1] === to) return true;
131
+ }
132
+ return false;
133
+ }
134
+
135
+ /* The same wall-clock time on a different day, IN THE ACCOUNT'S ZONE.
136
+
137
+ Built by asking the account's own formatter what instant renders as the
138
+ wanted wall time, never from the browser's offset: a page that guesses the
139
+ zone re-times a visit to a plausible wrong hour, which is the Task 2280
140
+ defect. Two passes, because the offset either side of a daylight-saving
141
+ transition differs and the first correction can land on the far side of it.
142
+
143
+ Returns the shape the door admits, which is what `toISOString` produces. */
144
+ function retimed(iso, dayKey, zone) {
145
+ var wall = new Intl.DateTimeFormat('en-GB', {
146
+ timeZone: zone,
147
+ hour: '2-digit',
148
+ minute: '2-digit',
149
+ hour12: false,
150
+ });
151
+ var held = wall.format(new Date(iso));
152
+ var guess = new Date(dayKey + 'T' + held + ':00.000Z');
153
+ for (var i = 0; i < 2; i++) {
154
+ var seen = wall.format(guess);
155
+ if (seen === held) break;
156
+ var drift =
157
+ (Number(held.slice(0, 2)) - Number(seen.slice(0, 2))) * 60 +
158
+ (Number(held.slice(3)) - Number(seen.slice(3)));
159
+ guess = new Date(guess.getTime() + drift * 60000);
160
+ }
161
+ return guess.toISOString();
162
+ }
163
+
164
+ /* One visit. Clicking moves it to the next declared status; dragging re-times
165
+ or re-assigns it. Both post through the portal's existing write door, and
166
+ both are refused here first against the published transition list. */
167
+ function visitButton(v, data, meta, zone) {
121
168
  var b = document.createElement('button');
122
169
  b.type = 'button';
123
170
  b.className = 'pl-visit';
171
+ b.draggable = true;
124
172
  b.textContent = [timeOf(v.startDate, zone), v.status, v.purpose].filter(Boolean).join(' · ');
125
173
  b.addEventListener('click', function () {
126
174
  advance(v, meta);
127
175
  });
176
+ /* The panel is a second gesture on the same card, so the click keeps
177
+ meaning what it meant before this task. */
178
+ b.addEventListener('contextmenu', function (e) {
179
+ e.preventDefault();
180
+ openVisit(v, data, meta, zone);
181
+ });
182
+ b.addEventListener('dragstart', function (e) {
183
+ dragging = v;
184
+ if (e.dataTransfer) e.dataTransfer.effectAllowed = 'move';
185
+ });
186
+ b.addEventListener('dragend', function () {
187
+ dragging = null;
188
+ });
128
189
  return b;
129
190
  }
130
191
 
192
+ /* The visit under the pointer during a drag. Held here rather than in
193
+ dataTransfer, which only carries strings and would mean looking the visit
194
+ back up by id on every drop. */
195
+ var dragging = null;
196
+
197
+ function move(v, meta, zone, toOwnerId, toDayKey) {
198
+ var body = { visitId: v.visitId, status: v.status };
199
+ if (toOwnerId !== v.ownerId) body.assignedOwnerId = toOwnerId;
200
+ if (toDayKey !== dayOf(v.startDate, zone)) {
201
+ body.startDate = retimed(v.startDate, toDayKey, zone);
202
+ }
203
+ /* A drop that changes neither is not a write. */
204
+ if (body.assignedOwnerId === undefined && body.startDate === undefined) return;
205
+ say('Saving…');
206
+ api('/api/visit-status', {
207
+ method: 'POST',
208
+ headers: { 'content-type': 'application/json' },
209
+ body: JSON.stringify(body),
210
+ })
211
+ .then(function (r) {
212
+ return r.json().then(function (d) {
213
+ return { s: r.status, d: d };
214
+ });
215
+ })
216
+ .then(function (res) {
217
+ if (!res.d.ok) {
218
+ console.error('[planner] move refused', { status: res.s, error: res.d.error });
219
+ say(
220
+ res.s === 404
221
+ ? 'That is not yours to change.'
222
+ : res.d.error === 'illegal-transition'
223
+ ? 'That move is not one this account allows.'
224
+ : 'That change was refused.',
225
+ );
226
+ /* The card returns to origin by redrawing what the device last
227
+ published. Leaving it where it fell would be the page telling the
228
+ operator something happened that did not. */
229
+ if (latest) render(latest);
230
+ return;
231
+ }
232
+ if (body.assignedOwnerId !== undefined) v.ownerId = body.assignedOwnerId;
233
+ if (body.startDate !== undefined) v.startDate = body.startDate;
234
+ say('Saved. It reaches the office within a minute.');
235
+ if (latest) render(latest);
236
+ })
237
+ .catch(function (err) {
238
+ console.error('[planner] move failed', { status: 'network', error: err });
239
+ say('Could not reach the server.');
240
+ if (latest) render(latest);
241
+ });
242
+ }
243
+
131
244
  function advance(v, meta) {
132
245
  var names = ((meta && meta.statuses) || []).map(function (s) {
133
246
  return s.name;
@@ -137,6 +250,10 @@
137
250
  return;
138
251
  }
139
252
  var next = names[(names.indexOf(v.status) + 1) % names.length];
253
+ if (!declared(meta, v.status, next)) {
254
+ say('That move is not one this account allows.');
255
+ return;
256
+ }
140
257
  say('Saving…');
141
258
  api('/api/visit-status', {
142
259
  method: 'POST',
@@ -169,6 +286,60 @@
169
286
  });
170
287
  }
171
288
 
289
+ /* Who may be sent to this record, why the others may not, and what has been
290
+ recorded against it.
291
+
292
+ Rendered from what /api/visits returned and nothing else. The install
293
+ computed the offers and the exclusions with the one implementation of
294
+ qualification and clash; re-deciding any of it here would be a second rule
295
+ to get wrong, which is what `functions/api/visits.ts` rules out. A
296
+ candidate carries no travel estimate today and is rendered without one
297
+ rather than dropped. */
298
+ function openVisit(v, data, meta, zone) {
299
+ var mine = (data.candidates || []).filter(function (c) {
300
+ return c.visitId === v.visitId;
301
+ });
302
+ document.getElementById('pl-allocate-head').textContent =
303
+ [timeOf(v.startDate, zone), v.status, v.purpose].filter(Boolean).join(' · ');
304
+
305
+ var list = document.getElementById('pl-allocate-list');
306
+ list.replaceChildren();
307
+ if (!mine.length) {
308
+ /* Never an empty box. Nobody offered and nobody excluded is a different
309
+ fact from an unstaffable record, and the operator cannot tell them
310
+ apart from a blank panel. */
311
+ var none = cell('pl-candidate', 'Nobody has been offered for this, and nobody was excluded.');
312
+ none.setAttribute('data-kind', 'excluded');
313
+ list.appendChild(none);
314
+ }
315
+ mine.forEach(function (c) {
316
+ var who = c.name || c.workerId;
317
+ var note =
318
+ c.kind === 'excluded'
319
+ ? c.reason
320
+ : (c.conflicts || []).length
321
+ ? (c.conflicts || []).length + ' already booked in this window'
322
+ : 'free';
323
+ var li = document.createElement('li');
324
+ li.className = 'pl-candidate';
325
+ li.setAttribute('data-kind', c.kind);
326
+ li.textContent = who + ' · ' + note;
327
+ list.appendChild(li);
328
+ });
329
+
330
+ /* What the worker recorded, as counts. The contents themselves are files,
331
+ and the portal's own file exchange already carries them. */
332
+ document.getElementById('pl-record').textContent = [
333
+ 'notes ' + (v.notes || 0),
334
+ 'media ' + (v.media || 0),
335
+ 'parts ' + (v.parts || 0),
336
+ 'checklist ' + (v.checklistDone || 0) + '/' + (v.checklistTotal || 0),
337
+ 'signatures ' + (v.signatures || 0),
338
+ ].join(' · ');
339
+
340
+ document.getElementById('pl-allocate').hidden = false;
341
+ }
342
+
172
343
  function render(data) {
173
344
  var meta = data.meta;
174
345
  /* UTC when the account published no config. Never the browser's: a page
@@ -225,12 +396,35 @@
225
396
  'data-off',
226
397
  w.workingHours && w.workingHours[HOUR_KEYS[i]] ? 'false' : 'true',
227
398
  );
399
+ /* Every cell is a drop target, including a day the worker does not
400
+ work: the office books into one deliberately, and a cell that
401
+ refused the drop would make that decision for them. A row with no
402
+ configured worker is not a target, because there is no ownerId to
403
+ re-assign to. */
404
+ if (w.workerId) {
405
+ c.addEventListener('dragover', function (e) {
406
+ if (!dragging) return;
407
+ e.preventDefault();
408
+ c.setAttribute('data-over', 'true');
409
+ });
410
+ c.addEventListener('dragleave', function () {
411
+ c.removeAttribute('data-over');
412
+ });
413
+ c.addEventListener('drop', function (e) {
414
+ e.preventDefault();
415
+ c.removeAttribute('data-over');
416
+ if (!dragging) return;
417
+ var v = dragging;
418
+ dragging = null;
419
+ move(v, meta, zone, w.ownerId, k);
420
+ });
421
+ }
228
422
  (byOwner[w.ownerId] || [])
229
423
  .filter(function (v) {
230
424
  return dayOf(v.startDate, zone) === k;
231
425
  })
232
426
  .forEach(function (v) {
233
- c.appendChild(visitButton(v, meta, zone));
427
+ c.appendChild(visitButton(v, data, meta, zone));
234
428
  });
235
429
  gridEl.appendChild(c);
236
430
  });
@@ -316,6 +510,10 @@
316
510
  });
317
511
  });
318
512
 
513
+ document.getElementById('pl-allocate-close').addEventListener('click', function () {
514
+ document.getElementById('pl-allocate').hidden = true;
515
+ });
516
+
319
517
  document.getElementById('pl-prev').addEventListener('click', function () {
320
518
  anchor.setUTCDate(anchor.getUTCDate() - 7);
321
519
  if (latest) render(latest);
@@ -406,7 +406,7 @@ Standing rules the operator gives over a channel are `Preference` nodes. Two mec
406
406
 
407
407
  **Reconcile by meaning on write (`profile-update`).** The existence check is two-tier. First an exact `(category, key)` match — the classic fast path, unchanged. When that misses, a semantic-neighbour lookup runs over the `preference_embedding` vector index, scoped to the same `accountId` and `scope:'admin'`, non-trashed. If the nearest neighbour is at or above `SAME_SUBJECT_COSINE` (0.85), the write lands on that node through the existing reinforce/update/contradict curve instead of minting a duplicate; the matched neighbours are returned in the tool result so the agent can classify future writes against real prior state. The confidence constants are unchanged — only the join key widens, so a rephrasing under a new `key` no longer forks the rule and a correction demotes the prior node rather than adding a contradictory one.
408
408
 
409
- **Inject the active rule-set every turn (channel gateways).** The one definition of "this account's active rules" is `@maxy/active-rules`: the account owner's admin-scope preferences (resolve the seeded `AdminUser {role:'owner'}`, then select through that owner's `UserProfile` via `HAS_PREFERENCE`), a floor-filtered (`confidence >= 0.4`), confidence-ordered query that returns every such preference verbatim — no cap, no de-confliction, no candidate limit. Concision is enforced at write time by the `conversational-memory` skill (each stored value is a single minimal imperative) and by the write-time semantic reconcile (a same-subject write at or above `SAME_SUBJECT_COSINE` folds into the existing node rather than minting a duplicate), so the injected block stays small because each stored preference is short and non-redundant, not because injection truncates. A preference owned by a non-owner profile (an operator's, stranded on the operator's own profile while managing the account) is never injected; an account with no seeded owner injects nothing. `profile-read` exposes this same owner-scoped set as `activeRules`, and the three inbound gateways (`wa-gateway`, `telegram-gateway`, `webchat-gateway`) fetch it for the effective session account and attach it as `standingRules` on the inbound frame; the channel builders prepend it as a `## Standing rules` block above `## Context`. Both channel-server inbound paths carry the block: the single-target SSE loop parses the whole frame, and the unified multi-target door reader (`runDoorStream`,) reconstructs it through the shared `doorInboundPayload` seam whose `parseFrame` closures read `standingRules` off the frame. The read is by code, not agent discretion, so a rule set last week is applied this week regardless of how many times the session compacted. Telegram and webchat inject for `role:'admin'` only — a public visitor never receives operator rules.
409
+ **Inject the active rule-set every turn (channel gateways).** The one definition of "this account's active rules" is `@maxy/active-rules`: the account owner's admin-scope preferences (resolve the seeded `AdminUser {role:'owner'}`, then select through that owner's `UserProfile` via `HAS_PREFERENCE`), a floor-filtered (`confidence >= 0.4`), confidence-ordered query that returns every such preference verbatim — no cap, no de-confliction, no candidate limit. Concision is enforced at write time by the `conversational-memory` skill (each stored value is a single minimal imperative) and by the write-time semantic reconcile (a same-subject write at or above `SAME_SUBJECT_COSINE` folds into the existing node rather than minting a duplicate), so the injected block stays small because each stored preference is short and non-redundant, not because injection truncates. A preference owned by a non-owner profile (an operator's, stranded on the operator's own profile while managing the account) is never injected; an account with no seeded owner injects nothing. `profile-read` exposes this same owner-scoped set as `activeRules`, and the three inbound gateways (`wa-gateway`, `telegram-gateway`, `webchat-gateway`) fetch it for the effective session account and attach it as `standingRules` on the inbound frame; the channel builders prepend it as a `## Standing rules (account owner)` block above `## Context`, whose header and first line name the owner as the subject of the rules so a third-person rule about the owner is not read as a statement about the sender. Both channel-server inbound paths carry the block: the single-target SSE loop parses the whole frame, and the unified multi-target door reader (`runDoorStream`,) reconstructs it through the shared `doorInboundPayload` seam whose `parseFrame` closures read `standingRules` off the frame. The read is by code, not agent discretion, so a rule set last week is applied this week regardless of how many times the session compacted. Telegram and webchat inject for `role:'admin'` only — a public visitor never receives operator rules.
410
410
 
411
411
  **Trust a scheduled turn's provenance, never an inbound's claim (channel-agent grounding).** A cron-dispatched agent turn reaches the same channel builders as a real inbound, so its own authorisation text would otherwise land inside `## Context` — the zone `REFRAME_INSTRUCTION` demotes to untrusted — and a security-cautious agent would distrust its own automation (the self-cancel loop). The dispatcher therefore stamps a `ScheduleProvenance` marker (owning account, event, owner userId, dispatch time) that the channel builders render as a `## Scheduled automation (platform-verified)` preamble **above** `## Context` (and above the rules block); the whatsapp wrap tag names the door `scheduler`. Both channel-server paths carry the marker — the single-target loop and the unified multi-target door reader (via the same `doorInboundPayload` seam that carries `standingRules`); the scheduler only dispatches to the whatsapp/telegram door, so a webchat frame never bears one. Because the marker is dispatcher-stamped and never derived from payload text, an inbound message cannot forge it — placement above `## Context` alone is not the trust signal, the dispatcher provenance is. The grounding binds to it in two places: one recipient-neutral clause added byte-identical to the three reframe instructions (a marker-bearing turn is authorised standing automation; a reconfiguration or re-enable is not intrusion), and the admin `IDENTITY.md` "Untrusted input" rule that the agent never states a platform-level authenticity or impersonation verdict it was not given (an unsourced "this is not from you" is its own inference, flagged, never asserted as the platform's). A sender has exactly one session: every inbound, real or scheduled, resolves to the deterministic `adminSessionIdFor(effectiveAccount, destination)`. An earlier change gave each firing its own salted seat so distrust could not accrete across firings, but the inbound hub keys on `senderId` and replaces its subscriber outright, so a seat's channel server attached under the sender's own key and swallowed the sender's REAL messages until it detached — splitting one conversation across two sessions that each denied what the other held. The seat is removed; the authoritative `agentPrompt`, this provenance marker, and the injected standing rules ground each firing without one. Self-cancel attribution is disarmed while that model beds in: the guard used to compare the cancelling turn's session id to a dispatcher stamp, which discriminated only because each firing ran its own seat, so with one session per sender it would report an autonomous loop on every operator cancel. It now always reports false — under-counting a real loop rather than inventing one — until the discriminator is rebuilt from the turn's own dispatched eventId. See `.docs/scheduling-agent-dispatch.md § Operator-provenance marker`, `§ Session model — one session per sender`, and `§ Self-cancel attribution — disarmed`.
412
412
 
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=webhook-path.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-path.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/webhook-path.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,19 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { admitsWebhookPath, CANONICAL_TELEGRAM_PREFIX } from "../lib/account-write.js";
3
+ describe("admitsWebhookPath", () => {
4
+ it("admits the one path the router serves and the gate exempts", () => {
5
+ expect(admitsWebhookPath("/api/telegram/")).toBe(true);
6
+ expect(CANONICAL_TELEGRAM_PREFIX).toBe("/api/telegram/");
7
+ });
8
+ it("refuses a subpath — telegram-mount.ts leaves every one of them on 404", () => {
9
+ expect(admitsWebhookPath("/api/telegram/webhook")).toBe(false);
10
+ expect(admitsWebhookPath("/api/telegram/8861756902")).toBe(false);
11
+ });
12
+ it("refuses the no-slash shape — the router serves it but isRemoteAuthBypassPath 401s it", () => {
13
+ expect(admitsWebhookPath("/api/telegram")).toBe(false);
14
+ });
15
+ it("refuses the official plugin's path", () => {
16
+ expect(admitsWebhookPath("/channels/telegram")).toBe(false);
17
+ });
18
+ });
19
+ //# sourceMappingURL=webhook-path.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-path.test.js","sourceRoot":"","sources":["../../src/__tests__/webhook-path.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,iBAAiB,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AAEvF,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IACjC,EAAE,CAAC,4DAA4D,EAAE,GAAG,EAAE;QACpE,MAAM,CAAC,iBAAiB,CAAC,gBAAgB,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACvD,MAAM,CAAC,yBAAyB,CAAC,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;IAC3D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uEAAuE,EAAE,GAAG,EAAE;QAC/E,MAAM,CAAC,iBAAiB,CAAC,uBAAuB,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC/D,MAAM,CAAC,iBAAiB,CAAC,0BAA0B,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACpE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sFAAsF,EAAE,GAAG,EAAE;QAC9F,MAAM,CAAC,iBAAiB,CAAC,eAAe,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACzD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,CAAC,iBAAiB,CAAC,oBAAoB,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC9D,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=webhook-register.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-register.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/webhook-register.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,110 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import { webhookRegister } from "../tools/webhook-register.js";
6
+ const STAMP = 1_754_600_000;
7
+ const TOKEN = "8861756902:AAAsecret";
8
+ const URL_OK = "https://sitedesk.example/api/telegram/";
9
+ /** account.json lives at <platformRoot>/../data/accounts/<id>/account.json. */
10
+ function stubAccount() {
11
+ const root = mkdtempSync(join(tmpdir(), "tg-reg-"));
12
+ const platformRoot = join(root, "platform");
13
+ mkdirSync(platformRoot, { recursive: true });
14
+ const dir = resolve(root, "data", "accounts", "acct-1");
15
+ mkdirSync(dir, { recursive: true });
16
+ const configPath = join(dir, "account.json");
17
+ writeFileSync(configPath, JSON.stringify({ telegram: { bots: [] } }));
18
+ return { platformRoot, configPath };
19
+ }
20
+ function botsIn(configPath) {
21
+ return JSON.parse(readFileSync(configPath, "utf-8")).telegram.bots;
22
+ }
23
+ /** getMe ok, setWebhook ok, getWebhookInfo returns whatever `info` says. */
24
+ function stubFetch(info) {
25
+ return (async (url) => ({
26
+ json: async () => {
27
+ if (String(url).endsWith("/getMe"))
28
+ return { ok: true, result: { username: "sitedeskbot" } };
29
+ if (String(url).endsWith("/setWebhook"))
30
+ return { ok: true };
31
+ return { result: { url: URL_OK, pending_update_count: 0, ...info } };
32
+ },
33
+ }));
34
+ }
35
+ function deps(platformRoot, lines, fetchImpl) {
36
+ const secretDir = mkdtempSync(join(tmpdir(), "tg-sec-"));
37
+ return {
38
+ platformRoot,
39
+ accountId: "acct-1",
40
+ log: (l) => lines.push(l),
41
+ fetchImpl,
42
+ nowSec: () => STAMP,
43
+ secretPathFor: (botId) => join(secretDir, botId),
44
+ };
45
+ }
46
+ describe("webhookRegister", () => {
47
+ it("refuses a subpath, names the one path that works, and writes nothing", async () => {
48
+ const { platformRoot, configPath } = stubAccount();
49
+ const lines = [];
50
+ const out = await webhookRegister({ botToken: TOKEN, webhookUrl: "https://sitedesk.example/api/telegram/webhook", role: "admin" }, deps(platformRoot, lines, stubFetch({})));
51
+ expect(out.isError).toBe(true);
52
+ expect(out.text).toContain("/api/telegram/");
53
+ expect(botsIn(configPath)).toEqual([]);
54
+ expect(lines.join("\n")).toContain("op=webhook-register path=/api/telegram/webhook");
55
+ expect(lines.join("\n")).toContain("admitted=false");
56
+ });
57
+ it("fails the registration on a fresh delivery error and persists no bot entry", async () => {
58
+ const { platformRoot, configPath } = stubAccount();
59
+ const lines = [];
60
+ const out = await webhookRegister({ botToken: TOKEN, webhookUrl: URL_OK, role: "admin" }, deps(platformRoot, lines, stubFetch({
61
+ last_error_date: STAMP + 3,
62
+ last_error_message: "Wrong response from the webhook: 404 Not Found",
63
+ })));
64
+ expect(out.isError).toBe(true);
65
+ expect(out.text).toContain("Wrong response from the webhook: 404 Not Found");
66
+ expect(botsIn(configPath)).toEqual([]);
67
+ expect(lines.join("\n")).toContain("op=webhook-verify");
68
+ expect(lines.join("\n")).toContain("reachable=false");
69
+ });
70
+ it("passes an error Telegram kept from the previous URL", async () => {
71
+ const { platformRoot, configPath } = stubAccount();
72
+ const lines = [];
73
+ const out = await webhookRegister({ botToken: TOKEN, webhookUrl: URL_OK, role: "admin" }, deps(platformRoot, lines, stubFetch({
74
+ last_error_date: STAMP - 60,
75
+ last_error_message: "Wrong response from the webhook: 404 Not Found",
76
+ pending_update_count: 83,
77
+ })));
78
+ expect(out.isError).toBe(false);
79
+ expect(botsIn(configPath)).toHaveLength(1);
80
+ expect(lines.join("\n")).toContain("pending=83");
81
+ });
82
+ it("persists the bot and logs the verify line on a clean probe", async () => {
83
+ const { platformRoot, configPath } = stubAccount();
84
+ const lines = [];
85
+ const out = await webhookRegister({ botToken: TOKEN, webhookUrl: URL_OK, role: "admin", adminUsers: [42] }, deps(platformRoot, lines, stubFetch({})));
86
+ expect(out.isError).toBe(false);
87
+ expect(botsIn(configPath)).toEqual([
88
+ { id: "8861756902", token: TOKEN, role: "admin", adminUsers: [42] },
89
+ ]);
90
+ expect(lines.join("\n")).toContain("op=webhook-verify path=/api/telegram/ botId=8861756902");
91
+ expect(lines.join("\n")).toContain("pending=0");
92
+ });
93
+ it("keeps the registration when the probe itself throws", async () => {
94
+ const { platformRoot, configPath } = stubAccount();
95
+ const lines = [];
96
+ const throwingProbe = (async (url) => {
97
+ if (String(url).endsWith("/getWebhookInfo"))
98
+ throw new Error("socket hang up");
99
+ return {
100
+ json: async () => String(url).endsWith("/getMe") ? { ok: true, result: { username: "b" } } : { ok: true },
101
+ };
102
+ });
103
+ const out = await webhookRegister({ botToken: TOKEN, webhookUrl: URL_OK, role: "admin" }, deps(platformRoot, lines, throwingProbe));
104
+ expect(out.isError).toBe(false);
105
+ expect(botsIn(configPath)).toHaveLength(1);
106
+ expect(lines.join("\n")).toContain("op=webhook-verify path=/api/telegram/ botId=8861756902 probe=failed");
107
+ expect(lines.join("\n")).toContain("reason=socket hang up");
108
+ });
109
+ });
110
+ //# sourceMappingURL=webhook-register.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-register.test.js","sourceRoot":"","sources":["../../src/__tests__/webhook-register.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=webhook-verdict.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-verdict.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/webhook-verdict.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { webhookVerdict } from "../lib/telegram.js";
3
+ const STAMP = 1_754_600_000;
4
+ const clean = { url: "https://x/api/telegram/?bot=1", pendingUpdateCount: 0, lastErrorDate: 0 };
5
+ describe("webhookVerdict", () => {
6
+ it("fails on an error Telegram saw at or after the registration stamp", () => {
7
+ expect(webhookVerdict({ ...clean, lastErrorDate: STAMP, lastErrorMessage: "Wrong response from the webhook: 404 Not Found" }, STAMP)).toEqual({ reachable: false, reason: "Wrong response from the webhook: 404 Not Found" });
8
+ });
9
+ it("passes an error dated before the stamp — it belongs to the previous URL", () => {
10
+ expect(webhookVerdict({ ...clean, lastErrorDate: STAMP - 1, lastErrorMessage: "Wrong response from the webhook: 404 Not Found" }, STAMP)).toEqual({ reachable: true, reason: "" });
11
+ });
12
+ it("passes a backlog with no error — pendingUpdateCount never gates", () => {
13
+ expect(webhookVerdict({ ...clean, pendingUpdateCount: 83 }, STAMP)).toEqual({
14
+ reachable: true,
15
+ reason: "",
16
+ });
17
+ });
18
+ it("passes a clean probe", () => {
19
+ expect(webhookVerdict(clean, STAMP)).toEqual({ reachable: true, reason: "" });
20
+ });
21
+ });
22
+ //# sourceMappingURL=webhook-verdict.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-verdict.test.js","sourceRoot":"","sources":["../../src/__tests__/webhook-verdict.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEpD,MAAM,KAAK,GAAG,aAAa,CAAC;AAE5B,MAAM,KAAK,GAAG,EAAE,GAAG,EAAE,+BAA+B,EAAE,kBAAkB,EAAE,CAAC,EAAE,aAAa,EAAE,CAAC,EAAE,CAAC;AAEhG,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;IAC9B,EAAE,CAAC,mEAAmE,EAAE,GAAG,EAAE;QAC3E,MAAM,CACJ,cAAc,CACZ,EAAE,GAAG,KAAK,EAAE,aAAa,EAAE,KAAK,EAAE,gBAAgB,EAAE,gDAAgD,EAAE,EACtG,KAAK,CACN,CACF,CAAC,OAAO,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,gDAAgD,EAAE,CAAC,CAAC;IAC5F,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yEAAyE,EAAE,GAAG,EAAE;QACjF,MAAM,CACJ,cAAc,CACZ,EAAE,GAAG,KAAK,EAAE,aAAa,EAAE,KAAK,GAAG,CAAC,EAAE,gBAAgB,EAAE,gDAAgD,EAAE,EAC1G,KAAK,CACN,CACF,CAAC,OAAO,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iEAAiE,EAAE,GAAG,EAAE;QACzE,MAAM,CAAC,cAAc,CAAC,EAAE,GAAG,KAAK,EAAE,kBAAkB,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC;YAC1E,SAAS,EAAE,IAAI;YACf,MAAM,EAAE,EAAE;SACX,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sBAAsB,EAAE,GAAG,EAAE;QAC9B,MAAM,CAAC,cAAc,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC;IAChF,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}