@segmentstream/agent 0.8.5 → 0.8.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.
package/README.md CHANGED
@@ -606,3 +606,14 @@ HTTP 503 with a correlation reference before durable queue admission. Telegram
606
606
  can redeliver the same source; the hook and attachment reservation must preserve
607
607
  idempotent source membership, storage results and the single acknowledgement.
608
608
  Such a failure must not become an HTTP success with no queued work.
609
+
610
+ ### Legacy held approvals
611
+
612
+ Explicit `confirmations.mode: "held"` overrides the deferred default. Switching a package version
613
+ does not migrate already-running held turns. In deferred mode the Telegram adapter reads the owning session's
614
+ recorded approval inputs and requests cancellation of that exact turn before admitting a new
615
+ addressed message. It never approves the old call or reuses the old member's authorization.
616
+ Cancellation failure requests webhook replay; terminal/resolved approvals are left alone.
617
+ Deferred mode retains its ten-minute database deadline and minute expiry sweep. Runtime health
618
+ reports the effective confirmation mode and attachment-lifecycle presence behind its existing
619
+ runtime authentication; source pins and public health alone are not settings-adoption proof.
@@ -4,7 +4,7 @@ import { createRequire as __eveCreateRequire } from "node:module";
4
4
  const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
- var version = "0.8.5";
7
+ var version = "0.8.6";
8
8
  function agentBuild(environment = process.env) {
9
9
  return {
10
10
  agent: version,
@@ -115,8 +115,9 @@ function present(value) {
115
115
  }
116
116
  function detectedHeader(rows) {
117
117
  const candidates = rows.slice(0, 50);
118
- const width = candidates.reduce((maximum, row) => Math.max(maximum, row.length), 0);
119
- const labels = candidates.findIndex((row) => row.length >= Math.max(1, width - 1) && row.some(present) && row.filter(present).every((value) => typeof value === "string" && numeric(value) === null && dateValue(value, "day_first") === null && dateValue(value, "month_first") === null));
118
+ const width = candidates.reduce((maximum, row) => Math.max(maximum, row.findLastIndex(present) + 1), 0);
119
+ const minimumLabels = Math.min(width, 2);
120
+ const labels = candidates.findIndex((row) => row.filter(present).length >= minimumLabels && row.some(present) && row.filter(present).every((value) => typeof value === "string" && numeric(value) === null && dateValue(value, "day_first") === null && dateValue(value, "month_first") === null));
120
121
  if (labels >= 0) return labels;
121
122
  return -1;
122
123
  }
@@ -4,5 +4,5 @@ import { createRequire as __eveCreateRequire } from "node:module";
4
4
  const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
- import { t as agentBuild } from "../../_chunks/build-metadata-BYl-5kFi.mjs";
7
+ import { t as agentBuild } from "../../_chunks/build-metadata-BIjXU_y3.mjs";
8
8
  export { agentBuild };
@@ -8,7 +8,7 @@ import { manifestOf } from "../../config.mjs";
8
8
  import { appOrigin, channelsEnabled, proxyEnvironment, runtimeIdentityEnvironment, webhookSecret } from "../environment.mjs";
9
9
  import { conversationLanguage, telegramPrincipal } from "./principal.mjs";
10
10
  import { chooseModel } from "../../model.mjs";
11
- import { t as agentBuild } from "../../../_chunks/build-metadata-BYl-5kFi.mjs";
11
+ import { t as agentBuild } from "../../../_chunks/build-metadata-BIjXU_y3.mjs";
12
12
  import { confirmationLocale } from "./confirmation-text.mjs";
13
13
  import { telegramSendScope } from "./send-queue.mjs";
14
14
  import { d as failureText, h as withDeliveryLocale, i as withReplacedText, m as safeTelegramFetch, n as currentInboundRequest, p as reportTelegramError, r as runInboundRequest, t as InboundRequest } from "../../../_chunks/inbound-request-B98tr4Pd.mjs";
@@ -166,6 +166,7 @@ function segmentstreamTelegram(options) {
166
166
  request: currentInboundRequest(),
167
167
  dependencies: {
168
168
  policy: settings().policy,
169
+ recoverHeldApprovals: options.confirmationMode?.() === "deferred",
169
170
  ...settings().attachmentLifecycle === void 0 ? {} : { lifecycle: (principal, memberId, message) => attachmentLifecycle({
170
171
  configuration: settings().attachmentLifecycle,
171
172
  principal,
@@ -252,7 +253,7 @@ function segmentstreamTelegram(options) {
252
253
  } catch (error) {
253
254
  recordGateDecision("inbound_message_failed");
254
255
  const errorId = reportTelegramError("inbound_message_failed", error, { chatId: message.chat.id });
255
- if (inbound?.lifecycleKey !== void 0) inbound.retryReference = errorId;
256
+ if (inbound?.retryReference !== void 0 || inbound?.lifecycleKey !== void 0) inbound.retryReference = errorId;
256
257
  else await context.telegram.post(failureText(errorId));
257
258
  return null;
258
259
  } finally {
@@ -394,7 +395,11 @@ function segmentstreamTelegram(options) {
394
395
  chats,
395
396
  enabled: () => channelsEnabled(environment),
396
397
  configured: () => options.settings() !== null,
397
- verify: () => verify ??= runtimeTokenVerifier(runtimeIdentityEnvironment(environment))
398
+ verify: () => verify ??= runtimeTokenVerifier(runtimeIdentityEnvironment(environment)),
399
+ configuration: () => ({
400
+ confirmationMode: options.confirmationMode?.() ?? null,
401
+ attachmentLifecycle: options.settings()?.attachmentLifecycle !== void 0
402
+ })
398
403
  })]
399
404
  };
400
405
  }
@@ -466,7 +471,7 @@ function gatedRoute(input) {
466
471
  await Promise.all(pending);
467
472
  }
468
473
  if (inbound.retryReference !== void 0) return Response.json({
469
- error: "attachment_intake_retry",
474
+ error: "telegram_turn_retry",
470
475
  reference: inbound.retryReference
471
476
  }, {
472
477
  status: 503,
@@ -11,6 +11,7 @@ export interface ConfirmationCopy {
11
11
  readonly cancelled: string;
12
12
  readonly failed: string;
13
13
  readonly expired?: string;
14
+ readonly interrupted?: string;
14
15
  readonly edit?: string;
15
16
  }
16
17
  export interface ConfirmationText {
@@ -23,6 +24,7 @@ export interface ConfirmationText {
23
24
  readonly completed: string;
24
25
  readonly failed: string;
25
26
  readonly expired: string;
27
+ readonly interrupted: string;
26
28
  readonly edit: string;
27
29
  }
28
30
  export declare function confirmationLocale(principal: SessionAuthContext | null, fallback?: string): string;
@@ -38,7 +38,11 @@ function confirmationCopy(value) {
38
38
  "cancelled",
39
39
  "failed"
40
40
  ];
41
- const optional = ["expired", "edit"];
41
+ const optional = [
42
+ "expired",
43
+ "interrupted",
44
+ "edit"
45
+ ];
42
46
  const valid = (key) => {
43
47
  const text = copy[key];
44
48
  return typeof text === "string" && text === text.trim() && (key === "question" || !/[✅❌⌛⚠❓✏]/u.test(text)) && humanText(text, key === "question" ? 300 : 24) === text;
@@ -72,6 +76,7 @@ function renderConfirmation(description, copy) {
72
76
  completed: copy === void 0 ? "✅" : `✅ ${copy.done}`,
73
77
  failed: copy === void 0 ? "⚠️" : `⚠️ ${copy.failed}`,
74
78
  expired: copy?.expired === void 0 ? "⌛" : `⌛ ${copy.expired}`,
79
+ interrupted: copy?.interrupted === void 0 ? "⏸" : `⏸ ${copy.interrupted}`,
75
80
  edit: copy?.edit === void 0 ? "✏️" : `✏️ ${copy.edit}`
76
81
  };
77
82
  }
@@ -70,7 +70,7 @@ function routingConfirmationState(events, prompt, at) {
70
70
  }
71
71
  }
72
72
  if (event.type === "input.resolved" && Array.isArray(data.resolutions)) {
73
- for (const resolution of data.resolutions) if (object(resolution).requestId === prompt.requestId) state = object(resolution).outcome === "approved" ? "approved" : "cancelled";
73
+ for (const resolution of data.resolutions) if (object(resolution).requestId === prompt.requestId) state = object(resolution).outcome === "approved" ? "approved" : object(resolution).outcome === "ignored" ? "interrupted" : "cancelled";
74
74
  }
75
75
  if (event.type === "action.result" && callId !== void 0 && object(data.result).callId === callId) state = data.status === "completed" ? "completed" : "failed";
76
76
  }
@@ -361,7 +361,7 @@ function telegramConfirmationEvents(archive, observed, deferredOptions) {
361
361
  const [callId, message] = entry;
362
362
  const copy = message.copy ?? renderConfirmation();
363
363
  const reason = confirmations.get()[callId]?.cancellationReason;
364
- const status = resolution.outcome === "approved" ? copy.executing : reason === "expired" ? copy.expired : copy.cancelled;
364
+ const status = resolution.outcome === "approved" ? copy.executing : reason === "expired" ? copy.expired : resolution.outcome === "ignored" ? copy.interrupted : copy.cancelled;
365
365
  const text = message.exactPost ? chatPostCardStatus(message.text, status) : `${message.text}\n\n${status}`;
366
366
  const edit = () => channel.telegram.editMessageText({
367
367
  messageId: message.messageId,
@@ -370,7 +370,7 @@ function telegramConfirmationEvents(archive, observed, deferredOptions) {
370
370
  });
371
371
  const prompt = {
372
372
  ...messagePrompt(message),
373
- state: resolution.outcome === "approved" ? "approved" : reason === "expired" ? "expired" : "cancelled"
373
+ state: resolution.outcome === "approved" ? "approved" : reason === "expired" ? "expired" : resolution.outcome === "ignored" ? "interrupted" : "cancelled"
374
374
  };
375
375
  message.prompt = prompt;
376
376
  await withConfirmationPrompt(prompt, () => message.exactPost ? withChatPostCard(text, edit, [...message.previewLinks ?? [], ...telegramTextLinks(status)]) : withConfirmationText(text, edit));
@@ -9,4 +9,8 @@ export declare function healthRoute(input: {
9
9
  readonly enabled: () => boolean;
10
10
  readonly configured: () => boolean;
11
11
  readonly verify: () => RuntimeTokenVerifier;
12
+ readonly configuration?: () => {
13
+ readonly confirmationMode: "deferred" | "held" | null;
14
+ readonly attachmentLifecycle: boolean;
15
+ };
12
16
  }): RouteDefinition<TelegramChannelState>;
@@ -4,7 +4,7 @@ import { createRequire as __eveCreateRequire } from "node:module";
4
4
  const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
- import { t as agentBuild } from "../../../_chunks/build-metadata-BYl-5kFi.mjs";
7
+ import { t as agentBuild } from "../../../_chunks/build-metadata-BIjXU_y3.mjs";
8
8
  import { runtimeRequestFailure } from "./runtime-request.mjs";
9
9
  import { GET } from "eve/channels";
10
10
  const HEALTH_ROUTE = "/eve/v1/telegram/health";
@@ -16,7 +16,8 @@ function healthRoute(input) {
16
16
  return Response.json({
17
17
  status: registry.degraded ? "degraded" : "ok",
18
18
  registry,
19
- runtime: agentBuild()
19
+ runtime: agentBuild(),
20
+ ...input.configuration === void 0 ? {} : { configuration: input.configuration() }
20
21
  }, {
21
22
  status: registry.degraded ? 503 : 200,
22
23
  headers: { "cache-control": "no-store" }
@@ -0,0 +1,27 @@
1
+ import type { CancelTurnResult } from "eve/channels";
2
+ import type { RecordedEvent } from "./sessions.js";
3
+ /** An approval is an input barrier, never permission to execute it while recovering a chat. */
4
+ export declare function heldApprovals(events: readonly RecordedEvent[]): {
5
+ requestId: string;
6
+ callId: string;
7
+ action: string;
8
+ turnId: string;
9
+ at: string;
10
+ }[];
11
+ /** Cancel the exact parked turn; never steer a new member into its permissions or old task. */
12
+ export declare function releaseHeldApproval(input: {
13
+ readonly events: readonly RecordedEvent[];
14
+ readonly addressed: boolean;
15
+ readonly now: number;
16
+ readonly cancel: (options: {
17
+ readonly turnId: string;
18
+ }) => Promise<CancelTurnResult>;
19
+ }): Promise<{
20
+ status: string;
21
+ reason: string;
22
+ turnId: string;
23
+ startedAt: string;
24
+ actions: string[];
25
+ cancellation: "accepted";
26
+ executionRequested: boolean;
27
+ } | undefined>;
@@ -0,0 +1,51 @@
1
+ import { fileURLToPath as __eveFileURLToPath } from "node:url";
2
+ import { dirname as __eveDirname } from "node:path";
3
+ import { createRequire as __eveCreateRequire } from "node:module";
4
+ const __filename = __eveFileURLToPath(import.meta.url);
5
+ __eveDirname(__filename);
6
+ __eveCreateRequire(import.meta.url);
7
+ import { object } from "../confirmation-description.mjs";
8
+ import { CONFIRMATION_TTL_MS } from "../confirmations.mjs";
9
+ function heldApprovals(events) {
10
+ const pending = new Map();
11
+ for (const event of events) {
12
+ const data = object(event.data);
13
+ if (event.type === "context.cleared" || event.type === "session.started") pending.clear();
14
+ if (event.type === "input.requested" && Array.isArray(data.requests)) for (const item of data.requests) {
15
+ const request = object(item);
16
+ const action = object(request.action);
17
+ if (request.kind === "tool-approval" && typeof request.requestId === "string" && typeof action.callId === "string" && typeof data.turnId === "string") pending.set(request.requestId, {
18
+ requestId: request.requestId,
19
+ callId: action.callId,
20
+ action: String(action.toolName ?? ""),
21
+ turnId: data.turnId,
22
+ at: event.meta.at
23
+ });
24
+ }
25
+ for (const [id, approval] of pending) if (event.type === "input.resolved" && Array.isArray(data.resolutions) && data.resolutions.some((item) => object(item).requestId === id) || event.type === "action.result" && object(data.result).callId === approval.callId || [
26
+ "turn.completed",
27
+ "turn.failed",
28
+ "turn.cancelled"
29
+ ].includes(event.type) && data.turnId === approval.turnId) pending.delete(id);
30
+ }
31
+ return [...pending.values()];
32
+ }
33
+ async function releaseHeldApproval(input) {
34
+ const pending = heldApprovals(input.events);
35
+ if (new Set(pending.map((approval) => approval.turnId)).size !== 1) return;
36
+ const expired = pending.some((approval) => input.now - Date.parse(approval.at) >= CONFIRMATION_TTL_MS);
37
+ if (!input.addressed && !expired) return;
38
+ const turnId = pending[0].turnId;
39
+ const result = await input.cancel({ turnId });
40
+ if (result.status === "no_active_turn") return;
41
+ return {
42
+ status: "cancellation_requested",
43
+ reason: expired ? "expired" : "addressed",
44
+ turnId,
45
+ startedAt: pending[0].at,
46
+ actions: pending.map((approval) => approval.action),
47
+ cancellation: result.status,
48
+ executionRequested: false
49
+ };
50
+ }
51
+ export { heldApprovals, releaseHeldApproval };
@@ -37,7 +37,7 @@ export declare class InboundRequest {
37
37
  stopTyping: (() => void) | undefined;
38
38
  lifecycle: ReturnType<typeof import("./lifecycle.js").attachmentLifecycle> | undefined;
39
39
  lifecycleKey: string | undefined;
40
- /** An admitted task must be redelivered if processing did not reach durable queue admission. */
40
+ /** A routed task must be redelivered if recovery or intake did not reach durable admission. */
41
41
  retryReference: string | undefined;
42
42
  lifecycleContext: string | undefined;
43
43
  monitor: ((session: Awaited<ReturnType<ChannelSource<unknown>["send"]>>, marker: string) => void) | undefined;
@@ -39,6 +39,8 @@ import type { PdfTextExtractor } from "./pdf-text.js";
39
39
  */
40
40
  export interface InboundDependencies {
41
41
  readonly policy: TelegramPolicy;
42
+ /** Recover old held sessions only when the workspace has chosen non-blocking confirmations. */
43
+ readonly recoverHeldApprovals?: boolean;
42
44
  readonly lifecycle?: (principal: import("eve/context").SessionAuthContext, memberId: string, message: TelegramMessage) => ReturnType<typeof attachmentLifecycle>;
43
45
  readonly admission: () => AdmissionSettings;
44
46
  readonly sessions: () => SessionSettings;
@@ -22,6 +22,7 @@ import { recordGateDecision, recordInboundContent, recordRouteDecision } from ".
22
22
  import { chatContext } from "./chats.mjs";
23
23
  import { admitMessage, localizedText } from "../admission.mjs";
24
24
  import { attachmentsContext, inboundAttachments, stageFiles } from "./files.mjs";
25
+ import { heldApprovals, releaseHeldApproval } from "./held-approvals.mjs";
25
26
  import { voiceOf, voiceText } from "./voice.mjs";
26
27
  const UNKNOWN_PRIVATE = "🔗";
27
28
  const UNKNOWN_IN_GROUP = "🔒";
@@ -721,6 +722,38 @@ async function handleInboundMessage(input) {
721
722
  recordGateDecision("message_already_handled");
722
723
  return null;
723
724
  }
725
+ if (dependencies.recoverHeldApprovals === true && operations !== void 0 && dependencies.history !== void 0) {
726
+ let recoverySessionId;
727
+ try {
728
+ const session = await operations.resolveSession(address);
729
+ if (session !== void 0) {
730
+ recoverySessionId = session.id;
731
+ const previous = session.id === owner?.id ? questionHistory : await dependencies.history(session.id);
732
+ const recovery = heldApprovals(previous).length === 0 ? void 0 : await releaseHeldApproval({
733
+ events: await dependencies.history(session.id),
734
+ addressed: addressed !== void 0,
735
+ now: Date.now(),
736
+ cancel: (options) => {
737
+ if (session.cancel === void 0) throw new Error("The parked session does not support turn cancellation.");
738
+ return session.cancel(options);
739
+ }
740
+ });
741
+ if (recovery !== void 0) {
742
+ recordGateDecision("held_approval_cancellation_requested", "reply");
743
+ notes.push(`<approval_recovery>${JSON.stringify(recovery)}</approval_recovery>`);
744
+ }
745
+ }
746
+ } catch (error) {
747
+ if (input.request !== void 0) {
748
+ input.request.retryReference = reportTelegramError("held_approval_release_failed", error, {
749
+ sessionId: recoverySessionId,
750
+ messageId: sent.messageId
751
+ });
752
+ await input.request.finishClaim?.(false);
753
+ }
754
+ throw error;
755
+ }
756
+ }
724
757
  if (voice !== void 0 && input.request !== void 0) input.request.text = text;
725
758
  else if (voice !== void 0) notes.push(text);
726
759
  if (answering && pending !== void 0 && input.request !== void 0) {
@@ -4,7 +4,7 @@ import { createRequire as __eveCreateRequire } from "node:module";
4
4
  const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
- import { t as agentBuild } from "../../../_chunks/build-metadata-BYl-5kFi.mjs";
7
+ import { t as agentBuild } from "../../../_chunks/build-metadata-BIjXU_y3.mjs";
8
8
  import { object } from "../confirmation-description.mjs";
9
9
  import { n as currentInboundRequest, p as reportTelegramError } from "../../../_chunks/inbound-request-B98tr4Pd.mjs";
10
10
  import { needsConfirmation } from "../confirmations.mjs";
@@ -62,7 +62,7 @@ export interface AgentMessage extends ObservedMessage {
62
62
  }
63
63
  /** Presentation only; no tool inputs, credential values or member identifiers. */
64
64
  export interface PendingConfirmation {
65
- readonly state?: "pending" | "approved" | "cancelled" | "expired" | "completed" | "failed";
65
+ readonly state?: "pending" | "approved" | "cancelled" | "interrupted" | "expired" | "completed" | "failed";
66
66
  readonly action: string;
67
67
  readonly requestedBy: string | null;
68
68
  readonly at: string;
@@ -1,4 +1,5 @@
1
1
  import type { TelegramMessage } from "eve/channels/telegram";
2
+ import type { CancelTurnResult } from "eve/channels";
2
3
  import type { SessionSettings } from "../../config.js";
3
4
  /**
4
5
  * Session policy for the Telegram channel (`agent-design.md`, section 4, and docs/agent.md,
@@ -56,6 +57,9 @@ export declare function historyOf(events: ReadonlyArray<RecordedEvent>): Session
56
57
  export interface SessionOperations {
57
58
  readonly resolveSession: (address: string) => Promise<{
58
59
  readonly id: string;
60
+ readonly cancel?: (options: {
61
+ readonly turnId: string;
62
+ }) => Promise<CancelTurnResult>;
59
63
  } | undefined>;
60
64
  readonly from: (address: string) => {
61
65
  compact(): Promise<unknown>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@segmentstream/agent",
3
- "version": "0.8.5",
3
+ "version": "0.8.6",
4
4
  "description": "Mount a SegmentStream workspace's agent on eve: the ontology as tools, acting as the member who writes, and Telegram through the workspace's connection.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "SegmentStream",
@@ -62,7 +62,7 @@
62
62
  },
63
63
  "dependencies": {
64
64
  "@chat-adapter/telegram": "4.41.0",
65
- "@segmentstream-cloud/ontology": "0.34.1",
65
+ "@segmentstream-cloud/ontology": "0.35.0",
66
66
  "@vercel/oidc": "^3.8.4",
67
67
  "ai": "^7.0.128",
68
68
  "csv-parse": "^7.0.3",