@segmentstream/agent 0.25.5 → 0.25.7

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
@@ -717,12 +717,15 @@ See [migration and verification](../../docs/agent-chat-delivery-migration.md).
717
717
 
718
718
  If Eve's session lookup raises `RunKeyFetchError` with HTTP 404 and Vercel's
719
719
  `not_found` / `Deployment not found.` response, the Telegram adapter records the
720
- original diagnostic and uses a fresh continuation scoped to the serving deployment.
721
- The smart gate still decides whether to reply; observing a message creates no session
722
- and sends no failure notice. Independent requests and cold starts select the same
723
- replacement. Healthy sessions continue across deployments as before. Once an address
724
- needs recovery, later deployments start it fresh; archived conversation context remains
720
+ original diagnostic and continues the address at a successor named by the address and
721
+ the unreachable run. The smart gate still decides whether to reply; observing a message
722
+ creates no session and sends no failure notice. Every deployment, independent request and
723
+ cold start selects the same successor, and Eve hands it over between deployments like any
724
+ healthy session. If the successor's own deployment is deleted later, the next successor
725
+ follows the same rule, up to four in a row. Archived conversation context remains
725
726
  available, but the inaccessible session's model history and pending inputs cannot migrate.
727
+ `segmentstream history squash` deletes earlier deployments and so ends the sessions
728
+ they own; each affected address continues once at its successor.
726
729
 
727
730
  Recovery applies only to session reads. Dispatch failures are never retried because
728
731
  the inbox may already have accepted them. Other provider errors propagate to operator
@@ -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.25.5";
7
+ var version = "0.25.7";
8
8
  function agentBuild(environment = process.env) {
9
9
  return {
10
10
  agent: version,
@@ -48,7 +48,8 @@ when it matters, tell the requester the action did not run and that they can ask
48
48
  the tool boundary's workspace-authored confirmation buttons. Call the action to start that flow; never ask for
49
49
  approval in prose or treat a text acknowledgement as approval. Wait for the button outcome.
50
50
  When the tool schema offers `_confirmation`, supply {question, confirm, cancel, done,
51
- cancelled, failed, expired?, edit?} in the chat's language. Describe the action and key
51
+ cancelled, failed, expired?, edit?} in the chat's language; a call needing a card without
52
+ valid copy is refused before any card. Describe the action and key
52
53
  parameters in a plain single-line question (at most 300 characters); labels are at most
53
54
  24 characters. No IDs, tool names, snake_case, URLs, credentials, or markup. The platform
54
55
  adds trusted facts. Omit this argument on older tools without schema support.
@@ -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-B_sSM5pA.mjs";
7
+ import { t as agentBuild } from "../../_chunks/build-metadata-Wt6SOKfE.mjs";
8
8
  export { agentBuild };
@@ -39,6 +39,11 @@ export declare const confirmations: StateHandle<Record<string, Confirmation>>;
39
39
  export declare function actionApproval(options: {
40
40
  readonly labels: (actor: OntologyActor, action: string, input: Readonly<Record<string, unknown>>) => Promise<ActionAnnotations>;
41
41
  readonly describe?: (actor: OntologyActor, action: string, input: Readonly<Record<string, unknown>>) => Promise<ConfirmationDescription>;
42
+ /**
43
+ * The call's `_confirmation` when the tool input does not carry it itself, such as a file-backed
44
+ * call whose arguments are snapshotted elsewhere. Defaults to the tool input's own field.
45
+ */
46
+ readonly copy?: (actor: OntologyActor, input: Readonly<Record<string, unknown>>) => unknown;
42
47
  readonly texts?: () => TelegramTexts;
43
48
  readonly state?: StateHandle<Record<string, Confirmation>>;
44
49
  readonly now?: () => number;
@@ -65,10 +65,13 @@ function actionApproval(options) {
65
65
  console.warn("The ontology action permission check failed.", error);
66
66
  return denied("The action is unavailable or the member no longer has permission.");
67
67
  }
68
- const copy = labels.confirmation === false ? void 0 : confirmationCopy(ctx.toolInput?._confirmation);
68
+ const offered = options.copy === void 0 ? ctx.toolInput?._confirmation : options.copy(member(memberId), ctx.toolInput ?? {});
69
+ const copy = labels.confirmation === false ? void 0 : confirmationCopy(offered);
69
70
  const businessInput = Object.fromEntries(Object.entries(ctx.toolInput ?? {}).filter(([key]) => key !== "_confirmation"));
70
71
  const posting = isChatPost(ctx.toolName);
71
72
  let description;
73
+ const missingCopy = () => denied((offered === void 0 ? "This action needs a confirmation card and the call has no _confirmation. " : "The call's _confirmation is invalid. ") + "Make the call again with _confirmation (for a file call, in its arguments file) in the chat's language: a plain single-line question naming the action and its key records and amounts (at most 300 characters), and short confirm, cancel, done, cancelled and failed labels without emoji.");
74
+ if (posting && saved === void 0 && labels.confirmation === true && copy === void 0) return missingCopy();
72
75
  if (posting) try {
73
76
  description = await options.describe?.(member(memberId), ctx.toolName, businessInput);
74
77
  const preview = description?.chatPost;
@@ -96,6 +99,7 @@ function actionApproval(options) {
96
99
  }
97
100
  if (saved !== void 0) return denied("The original call is waiting for its Confirm / Cancel buttons.");
98
101
  if (!posting && !needsConfirmation(labels)) return "approved";
102
+ if (labels.confirmation === true && copy === void 0) return missingCopy();
99
103
  try {
100
104
  if (!posting) description = await options.describe?.(member(memberId), ctx.toolName, businessInput);
101
105
  if (description === void 0) throw new Error("A trusted confirmation description is required.");
@@ -48,7 +48,15 @@ function ontologyFileCall(texts) {
48
48
  },
49
49
  describe: async (actor, _action, input) => {
50
50
  const saved = bound(input, ownerOf(actor));
51
- return ontologyTools({ principal }).describe(actor, saved.name, saved.arguments);
51
+ const business = Object.fromEntries(Object.entries(saved.arguments).filter(([key]) => key !== "_confirmation"));
52
+ return ontologyTools({ principal }).describe(actor, saved.name, business);
53
+ },
54
+ copy: (actor, input) => {
55
+ try {
56
+ return bound(input, ownerOf(actor)).arguments._confirmation;
57
+ } catch {
58
+ return;
59
+ }
52
60
  },
53
61
  texts
54
62
  });
@@ -70,7 +78,7 @@ function ontologyFileCall(texts) {
70
78
  }
71
79
  return defineTool({
72
80
  deferred: true,
73
- description: "Call an ontology tool with the complete JSON object computed in a sandbox file. Use for script-generated arguments, or set save_result to save a permitted read's exact JSON result in the isolated sandbox for scripts, without copying rows through the model. Find the real tool and its schema through Eve search first; ordinary inline calls use Eve. Chat posting is excluded. Saving a result never permits a write tool or expands query limits.",
81
+ description: "Call an ontology tool with the complete JSON object computed in a sandbox file. Use for script-generated arguments, or set save_result to save a permitted read's exact JSON result in the isolated sandbox for scripts, without copying rows through the model. Find the real tool and its schema through Eve search first; ordinary inline calls use Eve. Chat posting is excluded. Saving a result never permits a write tool or expands query limits. When the tool's schema offers _confirmation and the call needs a card, put it in the file.",
74
82
  inputSchema: {
75
83
  type: "object",
76
84
  properties: {
@@ -24,7 +24,7 @@ import { MAXIMUM_DOWNLOAD_BYTES, connectionProxy } from "../connection-proxy.mjs
24
24
  import { dispatchStructuredMessage, structuredMessage } from "./structured.mjs";
25
25
  import { scopedAddress, sessionPolicy } from "./sessions.mjs";
26
26
  import { handleWorkspaceCallback, isWorkspaceCallback } from "./callbacks.mjs";
27
- import { t as agentBuild } from "../../../_chunks/build-metadata-B_sSM5pA.mjs";
27
+ import { t as agentBuild } from "../../../_chunks/build-metadata-Wt6SOKfE.mjs";
28
28
  import { attachmentLifecycle } from "./lifecycle.mjs";
29
29
  import { gatewayImageDescriber } from "./images.mjs";
30
30
  import { gatewayAttachmentDescriber } from "./attachment-description.mjs";
@@ -446,7 +446,7 @@ function segmentstreamTelegram(options) {
446
446
  return {
447
447
  ...channel,
448
448
  async receive(input, operations) {
449
- operations = recoverableSessions(operations, environment.VERCEL_DEPLOYMENT_ID, readHistory);
449
+ operations = recoverableSessions(operations, readHistory);
450
450
  const target = input.target;
451
451
  if (target.confirmationSessionId !== void 0) {
452
452
  const address = target.confirmationAddress;
@@ -516,7 +516,7 @@ function gatedRoute(input) {
516
516
  if (options.settings() === null) return new Response("not found", { status: 404 });
517
517
  args = {
518
518
  ...args,
519
- ...recoverableSessions(args, environment.VERCEL_DEPLOYMENT_ID, input.readHistory)
519
+ ...recoverableSessions(args, input.readHistory)
520
520
  };
521
521
  let description;
522
522
  const describe = () => description ??= args.describe();
@@ -11,6 +11,8 @@ export interface TelegramTypingSource {
11
11
  export interface TypingCursor {
12
12
  readonly index: number;
13
13
  readonly turnId: string | null;
14
+ /** When the monitor first ran, in epoch milliseconds; continuations carry it forward. */
15
+ readonly startedAt?: number;
14
16
  }
15
17
  interface TypingEvent {
16
18
  readonly type: string;
@@ -40,6 +42,8 @@ export interface TypingBlockResult {
40
42
  readonly reason: string;
41
43
  readonly sent: number;
42
44
  }
45
+ /** The longest a turn shows typing; a longer turn keeps working without the indicator. */
46
+ export declare const MAX_TYPING_MS: number;
43
47
  /** One bounded activity. Its durable parent continues independently of the webhook host. */
44
48
  export declare function runTypingBlock(source: TelegramTypingSource, cursor: TypingCursor, dependencies: TypingBlockDependencies): Promise<TypingBlockResult>;
45
49
  export declare function telegramTypingBlock(source: TelegramTypingSource, cursor: TypingCursor, sessionStatus: string): Promise<TypingBlockResult>;
@@ -12,18 +12,22 @@ import { p as reportTelegramError } from "../../../_chunks/inbound-request-xmrm-
12
12
  import { connectionProxy } from "../connection-proxy.mjs";
13
13
  import { sessions } from "eve/server";
14
14
  const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
15
+ const MAX_TYPING_MS = 18e5;
15
16
  async function runTypingBlock(source, cursor, dependencies) {
16
17
  const deadline = dependencies.now() + 3e4;
18
+ const startedAt = cursor.startedAt ?? dependencies.now();
17
19
  let index = cursor.index, turnId = cursor.turnId ?? source.turnId ?? null, sent = 0;
18
20
  const result = (done, reason) => ({
19
21
  cursor: {
20
22
  index,
21
- turnId
23
+ turnId,
24
+ startedAt
22
25
  },
23
26
  done,
24
27
  reason,
25
28
  sent
26
29
  });
30
+ if (dependencies.now() - startedAt >= 18e5) return result(true, "typing_expired");
27
31
  if ([
28
32
  "completed",
29
33
  "failed",
@@ -175,4 +179,4 @@ async function recordTelegramTypingStart(source, workflowRunId) {
175
179
  });
176
180
  }
177
181
  }
178
- export { recordTelegramTypingStart, runTypingBlock, telegramTypingBlock };
182
+ export { MAX_TYPING_MS, recordTelegramTypingStart, runTypingBlock, telegramTypingBlock };
@@ -5,7 +5,7 @@ const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import { runtimeRequestFailure } from "./runtime-request.mjs";
8
- import { t as agentBuild } from "../../../_chunks/build-metadata-B_sSM5pA.mjs";
8
+ import { t as agentBuild } from "../../../_chunks/build-metadata-Wt6SOKfE.mjs";
9
9
  import { GET } from "eve/channels";
10
10
  const HEALTH_ROUTE = "/eve/v1/telegram/health";
11
11
  function healthRoute(input) {
@@ -10,7 +10,7 @@ import { n as currentInboundRequest, p as reportTelegramError } from "../../../_
10
10
  import { member } from "../actor.mjs";
11
11
  import { needsConfirmation } from "../confirmations.mjs";
12
12
  import { OntologyActionNotPermitted, actionToolName, ontologyTools } from "../ontology-tools.mjs";
13
- import { t as agentBuild } from "../../../_chunks/build-metadata-B_sSM5pA.mjs";
13
+ import { t as agentBuild } from "../../../_chunks/build-metadata-Wt6SOKfE.mjs";
14
14
  var AttachmentLifecycleNotPermitted = class extends Error {
15
15
  code = "attachment_lifecycle_not_permitted";
16
16
  };
@@ -2,9 +2,10 @@ import type { ChannelReceiveContext } from "eve/channels";
2
2
  import type { HistoryReader } from "./sessions.js";
3
3
  /**
4
4
  * A deleted deployment can make the Workflow metadata key unreadable before Eve can hand off a session.
5
- * Recover on a read, before dispatch: independent requests on this deployment use the same fresh
6
- * continuation. Healthy addresses keep their owner across deploys. An already broken address
7
- * gets a fresh owner on each deployment; the archive remains the source of routing context.
5
+ * Eve cannot reset that owner, so the address continues at a successor named by the address and the
6
+ * unreachable run. Every deployment derives the same successor, and Eve hands it over between
7
+ * deployments like any healthy session; if its own deployment is later deleted, the next successor
8
+ * follows the same rule. Recover on a read, before dispatch. Healthy addresses keep their owner.
8
9
  * No send is retried: a dispatch error can mean the durable inbox already accepted the message.
9
10
  */
10
- export declare function recoverableSessions<State>(operations: ChannelReceiveContext<State>, deploymentId: string | undefined, readHistory?: HistoryReader): ChannelReceiveContext<State>;
11
+ export declare function recoverableSessions<State>(operations: ChannelReceiveContext<State>, readHistory?: HistoryReader): ChannelReceiveContext<State>;
@@ -6,17 +6,18 @@ __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import { p as reportTelegramError } from "../../../_chunks/inbound-request-xmrm-49O.mjs";
8
8
  import { createHash } from "node:crypto";
9
- function missingDeployment(error) {
10
- if (!(error instanceof Error) || error.name !== "RunKeyFetchError" || !("status" in error) || error.status !== 404) return false;
11
- const body = /^Failed to fetch run key for wrun_[\w-]+ \(deployment dpl_[\w-]+\): HTTP 404 [^\n]+? — (.+)$/u.exec(error.message)?.[1];
9
+ function strandedRun(error) {
10
+ if (!(error instanceof Error) || error.name !== "RunKeyFetchError" || !("status" in error) || error.status !== 404) return;
11
+ const match = /^Failed to fetch run key for (wrun_[\w-]+) \(deployment dpl_[\w-]+\): HTTP 404 [^\n]+? — (.+)$/u.exec(error.message);
12
12
  try {
13
- const response = JSON.parse(body ?? "");
14
- return response?.error?.code === "not_found" && response.error.message === "Deployment not found.";
13
+ const response = JSON.parse(match?.[2] ?? "");
14
+ return response?.error?.code === "not_found" && response.error.message === "Deployment not found." ? match?.[1] : void 0;
15
15
  } catch {
16
- return false;
16
+ return;
17
17
  }
18
18
  }
19
- function recoverableSessions(operations, deploymentId, readHistory) {
19
+ const MAX_SUCCESSORS = 4;
20
+ function recoverableSessions(operations, readHistory) {
20
21
  const replacements = new Map();
21
22
  const readable = new Map();
22
23
  const target = (address) => replacements.get(address) ?? address;
@@ -33,18 +34,21 @@ function recoverableSessions(operations, deploymentId, readHistory) {
33
34
  return session;
34
35
  }
35
36
  async function resolveSession(address) {
36
- try {
37
- return await lookup(target(address));
37
+ let current = target(address);
38
+ for (let successors = 0;; successors += 1) try {
39
+ return await lookup(current);
38
40
  } catch (error) {
39
- if (!deploymentId || replacements.has(address) || !missingDeployment(error)) throw error;
40
- const replacement = `${address}:recovery:${createHash("sha256").update(JSON.stringify([address, deploymentId])).digest("hex")}`;
41
+ const run = successors < MAX_SUCCESSORS ? strandedRun(error) : void 0;
42
+ if (run === void 0) throw error;
43
+ const digest = createHash("sha256").update(JSON.stringify([current, run])).digest("hex");
44
+ const replacement = `${current}:recovery:${digest}`;
41
45
  reportTelegramError("session_resume_recovered", error, {
42
- address,
46
+ address: current,
43
47
  replacement,
44
- deploymentId
48
+ strandedRun: run
45
49
  });
46
50
  replacements.set(address, replacement);
47
- return lookup(replacement);
51
+ current = replacement;
48
52
  }
49
53
  }
50
54
  async function source(address) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@segmentstream/agent",
3
- "version": "0.25.5",
3
+ "version": "0.25.7",
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",