@vascend/talos 0.1.5 → 0.1.6

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 (224) hide show
  1. package/dist/core/agent.d.ts +16 -3
  2. package/dist/core/agent.js +114 -34
  3. package/dist/core/background.d.ts +10 -3
  4. package/dist/core/background.js +55 -9
  5. package/dist/core/bootstrap.js +1 -0
  6. package/dist/core/compaction.js +1 -1
  7. package/dist/core/config-storage.d.ts +5 -0
  8. package/dist/core/config-storage.js +83 -0
  9. package/dist/core/config.d.ts +11 -2
  10. package/dist/core/config.js +24 -8
  11. package/dist/core/context-ledger.js +2 -1
  12. package/dist/core/context-policy.d.ts +1 -1
  13. package/dist/core/context-policy.js +10 -7
  14. package/dist/core/credentials.js +29 -30
  15. package/dist/core/effort.d.ts +1 -1
  16. package/dist/core/effort.js +5 -1
  17. package/dist/core/execution-guidance.d.ts +1 -1
  18. package/dist/core/execution-guidance.js +2 -0
  19. package/dist/core/file-coordination.d.ts +3 -0
  20. package/dist/core/file-coordination.js +28 -0
  21. package/dist/core/file-lease-store.d.ts +3 -0
  22. package/dist/core/file-lease-store.js +28 -12
  23. package/dist/core/fusion.d.ts +24 -0
  24. package/dist/core/fusion.js +80 -0
  25. package/dist/core/model-limits.d.ts +1 -0
  26. package/dist/core/model-limits.js +5 -1
  27. package/dist/core/permission-queue.js +15 -15
  28. package/dist/core/request-plan.js +4 -3
  29. package/dist/core/session-naming.js +4 -0
  30. package/dist/core/session.d.ts +4 -2
  31. package/dist/core/session.js +13 -4
  32. package/dist/core/settings.d.ts +2 -0
  33. package/dist/core/settings.js +5 -16
  34. package/dist/core/step-policy.d.ts +18 -0
  35. package/dist/core/step-policy.js +6 -0
  36. package/dist/core/tool-artifacts.d.ts +2 -0
  37. package/dist/core/tool-artifacts.js +21 -3
  38. package/dist/core/tool-result-index.js +29 -33
  39. package/dist/core/tool-result-store.js +6 -5
  40. package/dist/core/types.d.ts +10 -3
  41. package/dist/core/user-inbox.d.ts +9 -0
  42. package/dist/core/user-inbox.js +22 -3
  43. package/dist/core/user-input.js +1 -1
  44. package/dist/fusion/autopilot-fast.d.ts +59 -0
  45. package/dist/fusion/autopilot-fast.js +213 -0
  46. package/dist/fusion/autopilot.d.ts +75 -0
  47. package/dist/fusion/autopilot.js +308 -0
  48. package/dist/fusion/focus.d.ts +30 -0
  49. package/dist/fusion/focus.js +122 -0
  50. package/dist/fusion/gate.d.ts +30 -0
  51. package/dist/fusion/gate.js +95 -0
  52. package/dist/fusion/jev.d.ts +71 -0
  53. package/dist/fusion/jev.js +164 -0
  54. package/dist/fusion/receipts.d.ts +25 -0
  55. package/dist/fusion/receipts.js +34 -0
  56. package/dist/fusion/route.d.ts +25 -0
  57. package/dist/fusion/route.js +84 -0
  58. package/dist/fusion/run.d.ts +4 -0
  59. package/dist/fusion/run.js +186 -0
  60. package/dist/horizon/engine.d.ts +20 -1
  61. package/dist/horizon/engine.js +57 -0
  62. package/dist/horizon/index.d.ts +2 -2
  63. package/dist/horizon/index.js +1 -1
  64. package/dist/horizon/types.d.ts +29 -0
  65. package/dist/index.js +12 -0
  66. package/dist/providers/anthropic-oauth.d.ts +8 -3
  67. package/dist/providers/anthropic-oauth.js +13 -10
  68. package/dist/providers/anthropic.d.ts +9 -2
  69. package/dist/providers/anthropic.js +64 -21
  70. package/dist/providers/cli-models.d.ts +30 -0
  71. package/dist/providers/cli-models.js +194 -0
  72. package/dist/providers/model-effort.js +1 -1
  73. package/dist/providers/openai-oauth.d.ts +8 -3
  74. package/dist/providers/openai-oauth.js +9 -3
  75. package/dist/providers/openrouter-manual-fallback.d.ts +3 -1
  76. package/dist/providers/openrouter-manual-fallback.js +20 -20
  77. package/dist/providers/registry.js +3 -2
  78. package/dist/providers/retry.d.ts +3 -0
  79. package/dist/providers/retry.js +44 -1
  80. package/dist/remote/cifratura.d.ts +12 -0
  81. package/dist/remote/cifratura.js +66 -0
  82. package/dist/remote/index.d.ts +15 -0
  83. package/dist/remote/index.js +65 -0
  84. package/dist/remote/relay-client.d.ts +58 -0
  85. package/dist/remote/relay-client.js +315 -0
  86. package/dist/remote/sessione-remota.d.ts +60 -0
  87. package/dist/remote/sessione-remota.js +266 -0
  88. package/dist/sdk.d.ts +7 -2
  89. package/dist/sdk.js +3 -1
  90. package/dist/tools/builtin.js +41 -20
  91. package/dist/tools/capabilities.d.ts +3 -1
  92. package/dist/tools/capabilities.js +5 -2
  93. package/dist/tools/computer-use.js +2 -1
  94. package/dist/tools/pagination.js +1 -1
  95. package/dist/tools/program.js +34 -21
  96. package/dist/tools/team-tools.js +11 -6
  97. package/dist/tui/account-entry.js +14 -9
  98. package/dist/tui/agent-label.d.ts +2 -0
  99. package/dist/tui/agent-label.js +5 -0
  100. package/dist/tui/agent-sessions-screen.d.ts +12 -2
  101. package/dist/tui/agent-sessions-screen.js +62 -16
  102. package/dist/tui/agent-sessions.d.ts +14 -0
  103. package/dist/tui/agent-sessions.js +67 -6
  104. package/dist/tui/bottom-input.d.ts +46 -2
  105. package/dist/tui/bottom-input.js +358 -59
  106. package/dist/tui/branding.js +8 -4
  107. package/dist/tui/capability-lists.d.ts +10 -0
  108. package/dist/tui/capability-lists.js +41 -0
  109. package/dist/tui/command-output-registry.js +4 -0
  110. package/dist/tui/command-transcript.d.ts +1 -1
  111. package/dist/tui/command-transcript.js +17 -21
  112. package/dist/tui/commands.js +4 -1
  113. package/dist/tui/compaction-status.d.ts +7 -0
  114. package/dist/tui/compaction-status.js +39 -0
  115. package/dist/tui/config-recovery.d.ts +12 -0
  116. package/dist/tui/config-recovery.js +54 -0
  117. package/dist/tui/context-meter.d.ts +3 -0
  118. package/dist/tui/context-meter.js +35 -8
  119. package/dist/tui/diff.d.ts +9 -0
  120. package/dist/tui/diff.js +43 -19
  121. package/dist/tui/fusion-menu.d.ts +5 -0
  122. package/dist/tui/fusion-menu.js +89 -0
  123. package/dist/tui/i18n-catalog.js +312 -1
  124. package/dist/tui/live-command.d.ts +1 -0
  125. package/dist/tui/live-command.js +9 -0
  126. package/dist/tui/live-output.d.ts +1 -0
  127. package/dist/tui/live-output.js +11 -0
  128. package/dist/tui/model-details.js +13 -12
  129. package/dist/tui/model-picker.js +17 -7
  130. package/dist/tui/model-selection.js +19 -10
  131. package/dist/tui/native-scrollback.js +5 -0
  132. package/dist/tui/prompt-queue.d.ts +3 -0
  133. package/dist/tui/prompt-queue.js +19 -1
  134. package/dist/tui/provider-label.d.ts +2 -0
  135. package/dist/tui/provider-label.js +9 -0
  136. package/dist/tui/provider-menu.js +25 -24
  137. package/dist/tui/queue-screen.js +2 -1
  138. package/dist/tui/remote-command.d.ts +7 -0
  139. package/dist/tui/remote-command.js +79 -0
  140. package/dist/tui/repl.d.ts +8 -2
  141. package/dist/tui/repl.js +355 -311
  142. package/dist/tui/review-store.d.ts +1 -0
  143. package/dist/tui/review-store.js +28 -7
  144. package/dist/tui/review-team.js +4 -3
  145. package/dist/tui/runtime-error.d.ts +2 -0
  146. package/dist/tui/runtime-error.js +26 -0
  147. package/dist/tui/selector-mouse.d.ts +1 -0
  148. package/dist/tui/selector-mouse.js +1 -1
  149. package/dist/tui/selector.d.ts +7 -0
  150. package/dist/tui/selector.js +19 -2
  151. package/dist/tui/session-controls.d.ts +26 -0
  152. package/dist/tui/session-controls.js +65 -0
  153. package/dist/tui/session-naming-notice.js +5 -3
  154. package/dist/tui/session-replay.d.ts +1 -0
  155. package/dist/tui/session-replay.js +101 -32
  156. package/dist/tui/session-transcript-layout.d.ts +9 -0
  157. package/dist/tui/session-transcript-layout.js +41 -0
  158. package/dist/tui/startup.js +63 -33
  159. package/dist/tui/status-line.d.ts +1 -1
  160. package/dist/tui/status-line.js +20 -12
  161. package/dist/tui/talos-art-data.d.ts +1 -0
  162. package/dist/tui/talos-art-data.js +2 -0
  163. package/dist/tui/talos-art.d.ts +1 -0
  164. package/dist/tui/talos-art.js +9 -0
  165. package/dist/tui/task-labels.d.ts +1 -0
  166. package/dist/tui/task-labels.js +8 -0
  167. package/dist/tui/terminal-text.d.ts +2 -1
  168. package/dist/tui/terminal-text.js +28 -2
  169. package/dist/tui/theme.js +2 -2
  170. package/dist/tui/tool-presentation.d.ts +3 -0
  171. package/dist/tui/tool-presentation.js +98 -0
  172. package/dist/tui/tool-style.js +4 -0
  173. package/dist/tui/transcript-store.d.ts +25 -0
  174. package/dist/tui/transcript-store.js +151 -0
  175. package/dist/tui/usage-footer.d.ts +1 -1
  176. package/dist/tui/usage-footer.js +3 -1
  177. package/dist/tui/vascend-id.d.ts +49 -0
  178. package/dist/tui/vascend-id.js +321 -0
  179. package/dist/tui/web-login-screen.js +4 -3
  180. package/dist/tui/web-login.d.ts +7 -15
  181. package/dist/tui/web-login.js +38 -78
  182. package/dist/tui/worker-transcripts.d.ts +31 -0
  183. package/dist/tui/worker-transcripts.js +91 -0
  184. package/dist/vascend/candidate-collector.d.ts +6 -5
  185. package/dist/vascend/candidate-collector.js +44 -43
  186. package/dist/vascend/context-lint.js +32 -2
  187. package/dist/vascend/context-market-selection.js +12 -5
  188. package/dist/vascend/context-pack-render.js +10 -4
  189. package/dist/vascend/context-packet-mirrors.d.ts +8 -0
  190. package/dist/vascend/context-packet-mirrors.js +24 -0
  191. package/dist/vascend/context-plan-events.d.ts +4 -0
  192. package/dist/vascend/context-plan-events.js +44 -0
  193. package/dist/vascend/context-snapshot.js +4 -2
  194. package/dist/vascend/context-store-reader.d.ts +20 -0
  195. package/dist/vascend/context-store-reader.js +46 -0
  196. package/dist/vascend/file-lock.d.ts +1 -0
  197. package/dist/vascend/file-lock.js +26 -0
  198. package/dist/vascend/granular-kingdom.d.ts +1 -0
  199. package/dist/vascend/granular-kingdom.js +22 -15
  200. package/dist/vascend/kingdom-bus.d.ts +1 -0
  201. package/dist/vascend/kingdom-bus.js +3 -0
  202. package/dist/vascend/kingdom-channel.d.ts +2 -0
  203. package/dist/vascend/kingdom-channel.js +3 -0
  204. package/dist/vascend/kingdom-live.d.ts +32 -4
  205. package/dist/vascend/kingdom-live.js +149 -13
  206. package/dist/vascend/kingdom-run.d.ts +5 -1
  207. package/dist/vascend/kingdom-run.js +6 -2
  208. package/dist/vascend/kingdom.d.ts +3 -0
  209. package/dist/vascend/kingdom.js +20 -13
  210. package/dist/vascend/memory-packet.d.ts +2 -1
  211. package/dist/vascend/memory-packet.js +31 -44
  212. package/dist/vascend/plan-store.d.ts +3 -1
  213. package/dist/vascend/plan-store.js +6 -4
  214. package/dist/vascend/room-memory.d.ts +10 -1
  215. package/dist/vascend/room-memory.js +42 -2
  216. package/dist/vascend/runtime.d.ts +3 -11
  217. package/dist/vascend/runtime.js +11 -123
  218. package/dist/vascend/session-search.js +2 -0
  219. package/dist/vascend/team-session.js +3 -2
  220. package/dist/vascend/vision.d.ts +3 -1
  221. package/dist/vascend/vision.js +6 -4
  222. package/dist/vascend/worker-session.d.ts +20 -0
  223. package/dist/vascend/worker-session.js +83 -0
  224. package/package.json +1 -1
@@ -194,6 +194,9 @@ export class RetryProvider {
194
194
  get models() {
195
195
  return this.inner.models;
196
196
  }
197
+ get modelLabels() {
198
+ return this.inner.modelLabels;
199
+ }
197
200
  get supportsFastMode() { return this.inner.supportsFastMode === true; }
198
201
  get netMax() {
199
202
  return this.cfg.networkMaxRetries ?? 0;
@@ -218,6 +221,31 @@ export class RetryProvider {
218
221
  req.onActivity?.(`Modello sovraccarico · nuovo tentativo (${nextAttempt}/${this.overloadMaxAttempts}) · ${seconds}s`);
219
222
  });
220
223
  }
224
+ async waitForSessionReset(req, limits) {
225
+ if (this.cfg.autoResumeSession === false)
226
+ return false;
227
+ const rejected = rejectedQuotaWindows(limits);
228
+ const resetAt = quotaResetAt(limits);
229
+ // Never guess a reset or wait on credit/weekly exhaustion. Every rejected
230
+ // window must be the session window, with a future reset supplied by the provider.
231
+ const session = rejected.find(w => w.id === "5h");
232
+ // Claude also rejects the aggregate unified window when its 5h window
233
+ // is exhausted. It is not an independent weekly/credit limit.
234
+ if (!session || rejected.some(w => w.id !== "5h" && w.id !== "unified") || resetAt === undefined || resetAt <= Date.now())
235
+ return false;
236
+ if (req.signal.aborted)
237
+ return true;
238
+ const zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
239
+ const time = new Date(resetAt).toLocaleTimeString("it-IT", { hour: "2-digit", minute: "2-digit", timeZone: zone });
240
+ req.onActivity?.(`Limite sessione di 5 ore raggiunto · reset alle ${time} (${zone}) · riprendo automaticamente al reset · Esc per annullare`);
241
+ // Short slices avoid timer overflow and remain responsive to clock changes.
242
+ while (!req.signal.aborted && Date.now() < resetAt) {
243
+ await delay(Math.min(60_000, resetAt - Date.now()), req.signal);
244
+ }
245
+ if (!req.signal.aborted)
246
+ req.onActivity?.("Limite sessione reimpostato · riprendo automaticamente il lavoro");
247
+ return true;
248
+ }
221
249
  async complete(req) {
222
250
  let netAttempt = 0;
223
251
  let genAttempt = 0;
@@ -248,13 +276,26 @@ export class RetryProvider {
248
276
  delivered = true;
249
277
  req.onToolDelta(event);
250
278
  } } : {}),
279
+ ...(req.onReasoning ? { onReasoning: (chunk) => {
280
+ if (!active || tm.signal.aborted)
281
+ return;
282
+ if (chunk)
283
+ delivered = true;
284
+ req.onReasoning(chunk);
285
+ } } : {}),
286
+ ...(req.onActivity ? { onActivity: (label) => {
287
+ if (active && !tm.signal.aborted)
288
+ req.onActivity(label);
289
+ } } : {}),
251
290
  });
252
291
  active = false;
253
292
  tm.cleanup();
254
293
  if (result.stopReason === "error") {
255
- // 1. Limite/quota abbonamento: stop, messaggio chiaro (no retry).
294
+ // 1. Sessione 5h: ripresa al reset solo se lo stream non e' iniziato.
256
295
  if (isQuotaError(result.text) || rejectedQuotaWindows(result.limits).length > 0) {
257
296
  const limits = quotaLimitsFromError(this.id, result.text, result.limits);
297
+ if (!delivered && await this.waitForSessionReset(req, limits))
298
+ continue;
258
299
  return { ...result, toolCalls: [], text: quotaMessage(this.id, result.text, limits), ...(limits ? { limits } : {}) };
259
300
  }
260
301
  if (delivered)
@@ -301,6 +342,8 @@ export class RetryProvider {
301
342
  const thrownText = err instanceof Error ? `${err.name}: ${err.message}` : String(err);
302
343
  if (!userAbort && isQuotaError(thrownText)) {
303
344
  const limits = quotaLimitsFromError(this.id, thrownText);
345
+ if (!delivered && await this.waitForSessionReset(req, limits))
346
+ continue;
304
347
  return { text: quotaMessage(this.id, thrownText, limits), toolCalls: [], stopReason: "error", ...(limits ? { limits } : {}) };
305
348
  }
306
349
  if (delivered)
@@ -0,0 +1,12 @@
1
+ export type Direzione = "ev" | "cmd";
2
+ export declare class CifraturaError extends Error {
3
+ readonly code: "chiave" | "corpo" | "autenticazione";
4
+ constructor(code: "chiave" | "corpo" | "autenticazione");
5
+ }
6
+ export declare function nuovaChiave(): Promise<{
7
+ readonly chiave: CryptoKey;
8
+ readonly testo: string;
9
+ }>;
10
+ export declare function importaChiave(testo: string): Promise<CryptoKey>;
11
+ export declare function cifra(chiave: CryptoKey, sessione: string, direzione: Direzione, dati: unknown): Promise<string>;
12
+ export declare function decifra(chiave: CryptoKey, sessione: string, direzione: Direzione, b: string): Promise<unknown>;
@@ -0,0 +1,66 @@
1
+ // Cifratura fra Talos e i dispositivi abbinati (telefono, web): AES-256-GCM.
2
+ //
3
+ // La chiave nasce qui, una per sessione, e arriva al telefono solo dentro il
4
+ // link di abbinamento, dopo il "#": il browser non manda mai quella parte a
5
+ // nessun server. Relay, Redis, Kafka e Flink vedono solo testo cifrato.
6
+ //
7
+ // Formato del campo `b`: base64url(iv 12 byte || testo cifrato con tag).
8
+ // I dati aggiuntivi autenticati legano ogni messaggio alla sessione e alla
9
+ // direzione: il relay non puo' spostare un evento in un'altra sessione ne'
10
+ // far passare un evento di Talos per un comando del telefono.
11
+ const TE = new TextEncoder();
12
+ const TD = new TextDecoder();
13
+ const VERSIONE = "vascend-relay/1";
14
+ export class CifraturaError extends Error {
15
+ code;
16
+ constructor(code) {
17
+ super(code);
18
+ this.code = code;
19
+ }
20
+ }
21
+ function subtle() {
22
+ const s = globalThis.crypto?.subtle;
23
+ if (!s)
24
+ throw new CifraturaError("chiave");
25
+ return s;
26
+ }
27
+ function aad(sessione, direzione) {
28
+ return TE.encode(`${VERSIONE}|${sessione}|${direzione}`);
29
+ }
30
+ // Chiave nuova e la sua forma da mettere nel link (base64url, 43 caratteri).
31
+ export async function nuovaChiave() {
32
+ const grezza = globalThis.crypto.getRandomValues(new Uint8Array(32));
33
+ const chiave = await subtle().importKey("raw", grezza, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
34
+ return { chiave, testo: Buffer.from(grezza).toString("base64url") };
35
+ }
36
+ export async function importaChiave(testo) {
37
+ if (!/^[A-Za-z0-9_-]{43}$/.test(testo))
38
+ throw new CifraturaError("chiave");
39
+ return subtle().importKey("raw", Buffer.from(testo, "base64url"), { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
40
+ }
41
+ export async function cifra(chiave, sessione, direzione, dati) {
42
+ const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
43
+ const ct = new Uint8Array(await subtle().encrypt({ name: "AES-GCM", iv, additionalData: aad(sessione, direzione) }, chiave, TE.encode(JSON.stringify(dati))));
44
+ const out = new Uint8Array(12 + ct.length);
45
+ out.set(iv);
46
+ out.set(ct, 12);
47
+ return Buffer.from(out).toString("base64url");
48
+ }
49
+ export async function decifra(chiave, sessione, direzione, b) {
50
+ if (typeof b !== "string" || b.length < 40 || !/^[A-Za-z0-9_-]+$/.test(b))
51
+ throw new CifraturaError("corpo");
52
+ const raw = Buffer.from(b, "base64url");
53
+ let chiaro;
54
+ try {
55
+ chiaro = await subtle().decrypt({ name: "AES-GCM", iv: raw.subarray(0, 12), additionalData: aad(sessione, direzione) }, chiave, raw.subarray(12));
56
+ }
57
+ catch {
58
+ throw new CifraturaError("autenticazione");
59
+ }
60
+ try {
61
+ return JSON.parse(TD.decode(chiaro));
62
+ }
63
+ catch {
64
+ throw new CifraturaError("corpo");
65
+ }
66
+ }
@@ -0,0 +1,15 @@
1
+ import type { AgentEvent } from "../core/types.js";
2
+ import { type StatoRelay } from "./relay-client.js";
3
+ import { SessioneRemota, type ManiRemote } from "./sessione-remota.js";
4
+ export { RelayError } from "./relay-client.js";
5
+ export type { StatoRelay } from "./relay-client.js";
6
+ export declare function collegaMani(m: ManiRemote | null): void;
7
+ export declare function sessioneRemota(): SessioneRemota | null;
8
+ export declare function avviaRemoto(env: NodeJS.ProcessEnv, cwd: string, eventi?: {
9
+ readonly onStato?: (stato: StatoRelay, motivo?: string) => void;
10
+ readonly onPresenza?: (n: number) => void;
11
+ }): Promise<SessioneRemota>;
12
+ export declare function fermaRemoto(): Promise<boolean>;
13
+ export declare function osservaEvento(event: AgentEvent): void;
14
+ export declare function messaggioUtente(testo: string): void;
15
+ export declare function permessoConGara(azione: string, dettaglio: string, signal: AbortSignal, tui: (signal: AbortSignal) => Promise<boolean>): Promise<boolean>;
@@ -0,0 +1,65 @@
1
+ // Controllo remoto di Talos: una sessione per processo, accesa con /remote.
2
+ // La TUI chiama queste funzioni nei suoi punti di passaggio (eventi
3
+ // dell'agente, messaggi dell'utente, permessi): a controllo spento non fanno
4
+ // niente e non costano niente.
5
+ import { RELAY_RESOURCE, tokenRisorsa } from "../tui/vascend-id.js";
6
+ import { RelayError, relayOf } from "./relay-client.js";
7
+ import { SessioneRemota } from "./sessione-remota.js";
8
+ export { RelayError } from "./relay-client.js";
9
+ let mani = null;
10
+ let attiva = null;
11
+ let avvio = null;
12
+ // La TUI si presenta una volta: senza mani (input classico) il remoto non parte.
13
+ export function collegaMani(m) { mani = m; }
14
+ export function sessioneRemota() { return attiva; }
15
+ export async function avviaRemoto(env, cwd, eventi = {}) {
16
+ if (attiva)
17
+ return attiva;
18
+ if (avvio)
19
+ return avvio;
20
+ if (!mani)
21
+ throw new RelayError("risposta");
22
+ const token = async () => {
23
+ const esito = await tokenRisorsa(env, RELAY_RESOURCE);
24
+ if (esito.token)
25
+ return esito.token;
26
+ throw new RelayError(esito.errore === "rete" ? "rete" : "token");
27
+ };
28
+ avvio = SessioneRemota.avvia({
29
+ base: relayOf(env),
30
+ cwd,
31
+ mani,
32
+ token,
33
+ onStato: (stato, motivo) => {
34
+ // Chiusa dal relay (altro dispositivo, piano scaduto): il remoto si spegne.
35
+ if (stato === "chiuso" && attiva)
36
+ attiva = null;
37
+ eventi.onStato?.(stato, motivo);
38
+ },
39
+ onPresenza: eventi.onPresenza,
40
+ });
41
+ try {
42
+ attiva = await avvio;
43
+ return attiva;
44
+ }
45
+ finally {
46
+ avvio = null;
47
+ }
48
+ }
49
+ export async function fermaRemoto() {
50
+ const s = attiva;
51
+ attiva = null;
52
+ if (!s)
53
+ return false;
54
+ await s.chiudi();
55
+ return true;
56
+ }
57
+ export function osservaEvento(event) {
58
+ attiva?.osserva(event);
59
+ }
60
+ export function messaggioUtente(testo) {
61
+ attiva?.messaggioUtente(testo);
62
+ }
63
+ export function permessoConGara(azione, dettaglio, signal, tui) {
64
+ return attiva ? attiva.permessoConGara(azione, dettaglio, signal, tui) : tui(signal);
65
+ }
@@ -0,0 +1,58 @@
1
+ export type StatoRelay = "connessione" | "collegato" | "attesa" | "chiuso";
2
+ export type CodiceRelay = "piano" | "token" | "troppe_sessioni" | "occupato" | "rete" | "risposta" | "finita";
3
+ export declare class RelayError extends Error {
4
+ readonly code: CodiceRelay;
5
+ readonly status?: number | undefined;
6
+ constructor(code: CodiceRelay, status?: number | undefined);
7
+ }
8
+ export interface ComandoRelay {
9
+ readonly id: string;
10
+ readonly cid: string;
11
+ readonly k: string;
12
+ readonly b?: string;
13
+ }
14
+ type FetchLike = (url: string, init?: RequestInit) => Promise<Response>;
15
+ type SocketCtor = new (url: string) => WebSocket;
16
+ export interface RelayDeps {
17
+ readonly token: () => Promise<string>;
18
+ readonly fetchImpl?: FetchLike;
19
+ readonly WebSocketImpl?: SocketCtor;
20
+ readonly wait?: (ms: number) => Promise<void>;
21
+ readonly onStato?: (stato: StatoRelay, motivo?: string) => void;
22
+ readonly onComando?: (comando: ComandoRelay) => Promise<void>;
23
+ readonly onPresenza?: (viewer: number) => void;
24
+ }
25
+ export declare const RELAY_URL = "https://relay.vascend.it";
26
+ export declare function relayOf(env: NodeJS.ProcessEnv): string;
27
+ export interface MetaSessione {
28
+ readonly title: string;
29
+ readonly agent: string;
30
+ readonly device: string;
31
+ }
32
+ export declare class RelayHost {
33
+ private readonly base;
34
+ readonly sessione: string;
35
+ private readonly deps;
36
+ private ws;
37
+ private stato;
38
+ private prossimo;
39
+ private readonly inAttesa;
40
+ private readonly eseguiti;
41
+ private catenaComandi;
42
+ private chiuso;
43
+ private tentativi;
44
+ private constructor();
45
+ static apri(base: string, meta: MetaSessione, deps: RelayDeps): Promise<RelayHost>;
46
+ statoAttuale(): StatoRelay;
47
+ invia(k: string, b?: string): boolean;
48
+ chiudi(attesaMs?: number): Promise<void>;
49
+ private cambiaStato;
50
+ private connetti;
51
+ private riconnetti;
52
+ private spedisci;
53
+ private daRelay;
54
+ private ciao;
55
+ private conferma;
56
+ private comando;
57
+ }
58
+ export {};
@@ -0,0 +1,315 @@
1
+ // Talos come host del relay delle sessioni (relay.vascend.it): possiede la
2
+ // sessione, trasmette gli eventi e riceve i comandi dei dispositivi abbinati.
3
+ // Protocollo: vascend-streaming/services/relay/src/protocollo.ts.
4
+ //
5
+ // - HTTP: POST /v1/sessions crea la sessione e da' un ticket da host;
6
+ // POST /v1/sessions/:id/tickets ne da' uno nuovo a ogni riconnessione.
7
+ // - WebSocket /v1/connect?ticket=...: il ticket vale 30 s e una volta sola,
8
+ // il token non finisce mai in un URL.
9
+ // - seq contigui da last_seq+1 (arriva nel hello), conferma cumulativa
10
+ // (ack). Gli eventi non confermati restano qui e si rimandano dopo una
11
+ // riconnessione; il relay scarta i doppioni.
12
+ // - i comandi si confermano (cmd_ack) dopo averli eseguiti: dopo una
13
+ // riconnessione il relay rimanda quelli non confermati, e qui si
14
+ // scartano per cid.
15
+ export class RelayError extends Error {
16
+ code;
17
+ status;
18
+ constructor(code, status) {
19
+ super(code);
20
+ this.code = code;
21
+ this.status = status;
22
+ }
23
+ }
24
+ export const RELAY_URL = "https://relay.vascend.it";
25
+ const TIMEOUT_MS = 12_000;
26
+ const ID_SESSIONE = /^s_[0-9a-f]{32}$/;
27
+ const TICKET = /^[0-9a-f]{64}$/;
28
+ const ID_STREAM = /^\d{1,15}-\d{1,10}$/;
29
+ const CID = /^[A-Za-z0-9_-]{8,64}$/;
30
+ // Oltre questi eventi non confermati si smette di mandare i delta (effimeri);
31
+ // gli eventi durevoli restano tutti, e si scarta solo sopra il tetto massimo.
32
+ const TETTO_DELTA = 2_000;
33
+ const TETTO_EVENTI = 20_000;
34
+ // Codici di chiusura del relay (connessioni.ts) dopo cui riprovare non serve.
35
+ const CHIUSURE_DEFINITIVE = {
36
+ 4001: "accesso non valido",
37
+ 4003: "accesso negato",
38
+ 4009: "sessione aperta da un altro dispositivo",
39
+ 4400: "messaggi non validi",
40
+ 4404: "sessione chiusa",
41
+ };
42
+ function urlValido(url) {
43
+ try {
44
+ const u = new URL(url);
45
+ if (u.username || u.password || u.search || u.hash)
46
+ return false;
47
+ if (u.protocol === "https:")
48
+ return true;
49
+ return u.protocol === "http:" && (u.hostname === "localhost" || u.hostname === "127.0.0.1");
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ }
55
+ // Override solo per sviluppo/collaudo (stesse regole dell'issuer di Vascend ID).
56
+ export function relayOf(env) {
57
+ const override = String(env["TALOS_RELAY_URL"] ?? "").trim();
58
+ return (override && urlValido(override) ? override : RELAY_URL).replace(/\/+$/, "");
59
+ }
60
+ export class RelayHost {
61
+ base;
62
+ sessione;
63
+ deps;
64
+ ws = null;
65
+ stato = "connessione";
66
+ prossimo = 1;
67
+ inAttesa = [];
68
+ eseguiti = new Set();
69
+ catenaComandi = Promise.resolve();
70
+ chiuso = false;
71
+ tentativi = 0;
72
+ constructor(base, sessione, deps) {
73
+ this.base = base;
74
+ this.sessione = sessione;
75
+ this.deps = deps;
76
+ }
77
+ // Crea la sessione sul relay e apre il collegamento.
78
+ static async apri(base, meta, deps) {
79
+ const data = await richiesta(base, deps, "POST", "/v1/sessions", meta);
80
+ const sessione = data["session"]?.["id"];
81
+ const ticket = data["ticket"];
82
+ if (typeof sessione !== "string" || !ID_SESSIONE.test(sessione) || typeof ticket !== "string" || !TICKET.test(ticket)) {
83
+ throw new RelayError("risposta");
84
+ }
85
+ const host = new RelayHost(base, sessione, deps);
86
+ host.connetti(ticket);
87
+ return host;
88
+ }
89
+ statoAttuale() { return this.stato; }
90
+ // Evento da trasmettere; `b` e' gia' cifrato. Falso se non e' stato accodato.
91
+ invia(k, b) {
92
+ if (this.chiuso)
93
+ return false;
94
+ if (this.inAttesa.length >= TETTO_EVENTI || (k === "delta" && this.inAttesa.length >= TETTO_DELTA))
95
+ return false;
96
+ const voce = { seq: this.prossimo++, k, b };
97
+ this.inAttesa.push(voce);
98
+ if (this.stato === "collegato")
99
+ this.spedisci(voce);
100
+ return true;
101
+ }
102
+ // Chiude per tutti: ultimi eventi (breve attesa della conferma), poi la
103
+ // sessione sul relay e il WebSocket.
104
+ async chiudi(attesaMs = 1_500) {
105
+ if (this.chiuso)
106
+ return;
107
+ const fine = Date.now() + attesaMs;
108
+ const wait = this.deps.wait ?? ((ms) => new Promise(r => setTimeout(r, ms)));
109
+ while (this.inAttesa.length > 0 && this.stato === "collegato" && Date.now() < fine)
110
+ await wait(50);
111
+ this.chiuso = true;
112
+ this.cambiaStato("chiuso");
113
+ try {
114
+ this.ws?.close(1000, "fine");
115
+ }
116
+ catch { /* gia' chiuso */ }
117
+ this.ws = null;
118
+ try {
119
+ await richiesta(this.base, this.deps, "DELETE", `/v1/sessions/${this.sessione}`);
120
+ }
121
+ catch { /* best-effort */ }
122
+ }
123
+ // --- collegamento -----------------------------------------------------------
124
+ cambiaStato(stato, motivo) {
125
+ if (this.stato === stato && !motivo)
126
+ return;
127
+ this.stato = stato;
128
+ this.deps.onStato?.(stato, motivo);
129
+ }
130
+ connetti(ticket) {
131
+ if (this.chiuso)
132
+ return;
133
+ const Ctor = this.deps.WebSocketImpl ?? globalThis.WebSocket;
134
+ if (!Ctor) {
135
+ this.chiuso = true;
136
+ this.cambiaStato("chiuso", "WebSocket non disponibile in questo runtime");
137
+ return;
138
+ }
139
+ const ws = new Ctor(`${this.base.replace(/^http/, "ws")}/v1/connect?ticket=${ticket}`);
140
+ this.ws = ws;
141
+ ws.addEventListener("message", (ev) => {
142
+ if (this.ws === ws && typeof ev.data === "string")
143
+ this.daRelay(ev.data);
144
+ });
145
+ ws.addEventListener("close", (ev) => {
146
+ if (this.ws !== ws)
147
+ return;
148
+ this.ws = null;
149
+ if (this.chiuso)
150
+ return;
151
+ const definitiva = CHIUSURE_DEFINITIVE[ev.code];
152
+ if (definitiva) {
153
+ this.chiuso = true;
154
+ this.cambiaStato("chiuso", definitiva);
155
+ return;
156
+ }
157
+ this.cambiaStato("attesa");
158
+ void this.riconnetti();
159
+ });
160
+ ws.addEventListener("error", () => { });
161
+ }
162
+ async riconnetti() {
163
+ const wait = this.deps.wait ?? ((ms) => new Promise(r => setTimeout(r, ms)));
164
+ while (!this.chiuso) {
165
+ const attesa = Math.min(30_000, 1_000 * 2 ** Math.min(this.tentativi, 5));
166
+ this.tentativi++;
167
+ await wait(attesa);
168
+ if (this.chiuso)
169
+ return;
170
+ try {
171
+ const data = await richiesta(this.base, this.deps, "POST", `/v1/sessions/${this.sessione}/tickets`, { role: "host" });
172
+ const ticket = data["ticket"];
173
+ if (typeof ticket !== "string" || !TICKET.test(ticket))
174
+ throw new RelayError("risposta");
175
+ this.cambiaStato("connessione");
176
+ this.connetti(ticket);
177
+ return;
178
+ }
179
+ catch (e) {
180
+ const code = e instanceof RelayError ? e.code : "rete";
181
+ // Sessione chiusa o piano scaduto: inutile riprovare.
182
+ if (code === "finita" || code === "piano" || code === "token") {
183
+ this.chiuso = true;
184
+ this.cambiaStato("chiuso", code);
185
+ return;
186
+ }
187
+ }
188
+ }
189
+ }
190
+ spedisci(voce) {
191
+ try {
192
+ this.ws?.send(JSON.stringify({ t: "ev", seq: voce.seq, k: voce.k, b: voce.b }));
193
+ }
194
+ catch { /* ripreso al prossimo hello */ }
195
+ }
196
+ daRelay(raw) {
197
+ let f;
198
+ try {
199
+ f = JSON.parse(raw);
200
+ }
201
+ catch {
202
+ return;
203
+ }
204
+ if (!f || typeof f !== "object")
205
+ return;
206
+ switch (f["t"]) {
207
+ case "hello": return this.ciao(Number(f["last_seq"]));
208
+ case "ack": return this.conferma(Number(f["seq"]));
209
+ case "cmd": return this.comando(f);
210
+ case "presence":
211
+ this.deps.onPresenza?.(Number(f["viewers"]) || 0);
212
+ return;
213
+ default: return;
214
+ }
215
+ }
216
+ ciao(ultimo) {
217
+ const salvato = Number.isSafeInteger(ultimo) && ultimo >= 0 ? ultimo : 0;
218
+ this.conferma(salvato);
219
+ // Il relay ha perso degli eventi (es. Redis svuotato): si rinumera da li'
220
+ // invece di restare fuori sequenza per sempre.
221
+ if (this.inAttesa.length > 0 && this.inAttesa[0].seq !== salvato + 1) {
222
+ this.inAttesa.forEach((v, i) => { v.seq = salvato + 1 + i; });
223
+ }
224
+ this.prossimo = Math.max(this.prossimo, salvato + 1 + this.inAttesa.length);
225
+ if (this.inAttesa.length === 0)
226
+ this.prossimo = Math.max(salvato + 1, this.prossimo);
227
+ this.tentativi = 0;
228
+ this.cambiaStato("collegato");
229
+ for (const v of this.inAttesa)
230
+ this.spedisci(v);
231
+ }
232
+ conferma(seq) {
233
+ if (!Number.isSafeInteger(seq))
234
+ return;
235
+ while (this.inAttesa.length > 0 && this.inAttesa[0].seq <= seq)
236
+ this.inAttesa.shift();
237
+ }
238
+ comando(f) {
239
+ const id = f["id"], cid = f["cid"], k = f["k"], b = f["b"];
240
+ if (typeof id !== "string" || !ID_STREAM.test(id) || typeof cid !== "string" || !CID.test(cid) || typeof k !== "string")
241
+ return;
242
+ const ws = this.ws;
243
+ const ack = () => { try {
244
+ if (this.ws === ws)
245
+ ws?.send(JSON.stringify({ t: "cmd_ack", id }));
246
+ }
247
+ catch { /* rimandato dal relay */ } };
248
+ if (this.eseguiti.has(cid)) {
249
+ ack();
250
+ return;
251
+ }
252
+ // In ordine, uno alla volta: un "stop" dopo un messaggio deve arrivare dopo.
253
+ this.catenaComandi = this.catenaComandi.then(async () => {
254
+ if (this.eseguiti.has(cid))
255
+ return ack();
256
+ this.eseguiti.add(cid);
257
+ if (this.eseguiti.size > 5_000)
258
+ this.eseguiti.delete(this.eseguiti.values().next().value);
259
+ try {
260
+ await this.deps.onComando?.({ id, cid, k, b: typeof b === "string" ? b : undefined });
261
+ }
262
+ catch { /* un comando rotto non ferma gli altri */ }
263
+ ack();
264
+ });
265
+ }
266
+ }
267
+ // --- HTTP ---------------------------------------------------------------------
268
+ async function richiesta(base, deps, method, path, body) {
269
+ const token = await deps.token();
270
+ const f = deps.fetchImpl ?? fetch;
271
+ let response;
272
+ let raw;
273
+ try {
274
+ response = await f(base + path, {
275
+ method,
276
+ headers: {
277
+ Authorization: `Bearer ${token}`,
278
+ Accept: "application/json",
279
+ ...(body === undefined ? {} : { "Content-Type": "application/json" }),
280
+ },
281
+ body: body === undefined ? undefined : JSON.stringify(body),
282
+ redirect: "error",
283
+ signal: AbortSignal.timeout(TIMEOUT_MS),
284
+ });
285
+ raw = await response.text();
286
+ }
287
+ catch {
288
+ throw new RelayError("rete");
289
+ }
290
+ if (raw.length > 65_536)
291
+ throw new RelayError("risposta", response.status);
292
+ if (!response.ok) {
293
+ const s = response.status;
294
+ if (s === 401)
295
+ throw new RelayError("token", s);
296
+ if (s === 402)
297
+ throw new RelayError("piano", s);
298
+ if (s === 404 || s === 409)
299
+ throw new RelayError("finita", s);
300
+ if (s === 429)
301
+ throw new RelayError("troppe_sessioni", s);
302
+ if (s === 503)
303
+ throw new RelayError("occupato", s);
304
+ throw new RelayError(s >= 500 ? "rete" : "risposta", s);
305
+ }
306
+ if (!raw)
307
+ return {};
308
+ try {
309
+ const data = JSON.parse(raw);
310
+ if (data && typeof data === "object" && !Array.isArray(data))
311
+ return data;
312
+ }
313
+ catch { /* sotto */ }
314
+ throw new RelayError("risposta", response.status);
315
+ }
@@ -0,0 +1,60 @@
1
+ import type { AgentEvent } from "../core/types.js";
2
+ import { type RelayDeps, type StatoRelay } from "./relay-client.js";
3
+ export declare const VIEWER_URL = "https://vascend.it/remoto";
4
+ export interface ManiRemote {
5
+ readonly invia: (testo: string, subito: boolean) => boolean;
6
+ readonly ferma: () => boolean;
7
+ }
8
+ export interface AvvioRemoto {
9
+ readonly base: string;
10
+ readonly cwd: string;
11
+ readonly mani: ManiRemote;
12
+ readonly token: RelayDeps["token"];
13
+ readonly onStato?: (stato: StatoRelay, motivo?: string) => void;
14
+ readonly onPresenza?: (viewer: number) => void;
15
+ readonly relayDeps?: Omit<RelayDeps, "token" | "onStato" | "onComando" | "onPresenza">;
16
+ readonly now?: () => number;
17
+ }
18
+ export declare class SessioneRemota {
19
+ private readonly relay;
20
+ private readonly chiave;
21
+ readonly link: string;
22
+ private readonly mani;
23
+ private readonly now;
24
+ private catena;
25
+ private delta;
26
+ private timerDelta;
27
+ private risposta;
28
+ private viewer;
29
+ private readonly permessi;
30
+ private constructor();
31
+ static avvia(opts: AvvioRemoto): Promise<SessioneRemota>;
32
+ get id(): string;
33
+ statoRelay(): StatoRelay;
34
+ dispositivi(): number;
35
+ osserva(event: AgentEvent): void;
36
+ testo(delta: string): void;
37
+ rispostaCompleta(): void;
38
+ chiamataTool(c: {
39
+ readonly id: string;
40
+ readonly name: string;
41
+ readonly input: unknown;
42
+ readonly parent?: string;
43
+ }): void;
44
+ risultatoTool(r: {
45
+ readonly id: string;
46
+ readonly error: boolean;
47
+ readonly output: string;
48
+ readonly parent?: string;
49
+ }): void;
50
+ statoAgente(state: "thinking" | "working" | "waiting" | "idle" | "error", label?: string): void;
51
+ messaggioUtente(testo: string): void;
52
+ permessoConGara(azione: string, dettaglio: string, signal: AbortSignal, tui: (signal: AbortSignal) => Promise<boolean>): Promise<boolean>;
53
+ chiudi(): Promise<void>;
54
+ private comando;
55
+ private svuotaDelta;
56
+ private chiudiRisposta;
57
+ private stato;
58
+ private evento;
59
+ private accoda;
60
+ }