eve 0.81.0 → 0.81.1

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 (76) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/src/compiled/.vendor-stamp.json +2 -2
  3. package/dist/src/compiled/@eve/catalog/index.js +1 -1
  4. package/dist/src/eve-channel/create-request.d.ts +2 -1
  5. package/dist/src/eve-channel/index.js +1 -1
  6. package/dist/src/eve-channel/request.d.ts +2 -1
  7. package/dist/src/eve-channel/request.js +1 -2
  8. package/dist/src/eve-channel/support.d.ts +9 -1
  9. package/dist/src/eve-channel/support.js +2 -1
  10. package/dist/src/eve-channel/types.d.ts +18 -0
  11. package/dist/src/execution/dynamic-workflow/tool.js +1 -1
  12. package/dist/src/execution/hook-ownership.d.ts +8 -0
  13. package/dist/src/execution/hook-ownership.js +1 -1
  14. package/dist/src/execution/session/entry-input.d.ts +19 -5
  15. package/dist/src/execution/session/entry-input.js +1 -1
  16. package/dist/src/execution/session/entry.js +1 -1
  17. package/dist/src/execution/session/handoff-steps.d.ts +75 -13
  18. package/dist/src/execution/session/handoff-steps.js +1 -1
  19. package/dist/src/execution/session/handoff.d.ts +69 -27
  20. package/dist/src/execution/session/handoff.js +1 -1
  21. package/dist/src/execution/session/program.js +1 -1
  22. package/dist/src/execution/session/run-watch.d.ts +22 -0
  23. package/dist/src/execution/session/run-watch.js +1 -0
  24. package/dist/src/execution/session-inbox/address.d.ts +4 -1
  25. package/dist/src/execution/session-inbox/end-stranded-session.js +1 -1
  26. package/dist/src/execution/session-inbox/hook-release.d.ts +9 -2
  27. package/dist/src/execution/session-inbox/hook-release.js +1 -1
  28. package/dist/src/execution/session-inbox/inbox.d.ts +66 -6
  29. package/dist/src/execution/session-inbox/inbox.js +1 -1
  30. package/dist/src/execution/session-inbox/resume.d.ts +4 -3
  31. package/dist/src/execution/tools/agent-router-workflow.js +1 -1
  32. package/dist/src/execution/workflow-runtime.d.ts +6 -11
  33. package/dist/src/execution/workflow-runtime.js +1 -1
  34. package/dist/src/harness/model-call/call.js +1 -1
  35. package/dist/src/instrumentation/content.d.ts +1 -1
  36. package/dist/src/instrumentation/content.js +1 -1
  37. package/dist/src/instrumentation/lifecycle.d.ts +4 -2
  38. package/dist/src/internal/application/stamped-package-version.js +1 -1
  39. package/dist/src/internal/authored-definition/connection.js +1 -1
  40. package/dist/src/internal/mcp/skills.js +1 -1
  41. package/dist/src/internal/workflow/development-world-client.js +1 -1
  42. package/dist/src/public/channels/chat-sdk/messageToUserContent.d.ts +2 -1
  43. package/dist/src/public/channels/chat-sdk/messageToUserContent.js +1 -1
  44. package/dist/src/public/channels/photon/attachments.d.ts +5 -2
  45. package/dist/src/public/channels/photon/attachments.js +1 -1
  46. package/dist/src/public/channels/photon/photonIMessageChannel.js +1 -1
  47. package/dist/src/public/channels/slack/api.d.ts +8 -1
  48. package/dist/src/public/channels/slack/api.js +1 -1
  49. package/dist/src/public/channels/slack/slackChannel.js +1 -1
  50. package/dist/src/setup/scaffold/create/project.js +1 -1
  51. package/dist/src/shared/gateway-routing.d.ts +20 -0
  52. package/dist/src/shared/gateway-routing.js +1 -0
  53. package/dist/src/shared/skill-package.js +1 -1
  54. package/dist/src/tools/provided/ask-question-workflow.js +1 -1
  55. package/dist/src/tools/provided/sleep-workflow.js +1 -1
  56. package/dist/src/tracing/eve/agent-otel-provider.js +1 -1
  57. package/dist/src/tracing/eve/gateway.d.ts +5 -0
  58. package/dist/src/tracing/eve/gateway.js +1 -1
  59. package/dist/src/tracing/eve/operation-input.d.ts +0 -7
  60. package/dist/src/tracing/eve/operation-input.js +1 -1
  61. package/dist/src/tracing/eve/policy.d.ts +1 -1
  62. package/dist/src/tracing/eve/policy.js +1 -1
  63. package/dist/src/tracing/eve/session.d.ts +2 -0
  64. package/dist/src/tracing/eve/session.js +1 -1
  65. package/docs/README.md +1 -0
  66. package/docs/agent-config.md +22 -0
  67. package/docs/channels/chat-sdk.mdx +4 -2
  68. package/docs/channels/eve.mdx +27 -0
  69. package/docs/channels/photon.mdx +4 -2
  70. package/docs/channels/slack.mdx +2 -0
  71. package/docs/concepts/execution-model-and-durability.mdx +16 -7
  72. package/docs/guides/instrumentation/otel.mdx +20 -0
  73. package/docs/patterns/meta.json +1 -0
  74. package/docs/patterns/voice-notes.md +67 -0
  75. package/docs/sandbox/index.mdx +2 -0
  76. package/package.json +1 -1
@@ -1,30 +1,68 @@
1
+ import type { SessionHandoverPayload } from "#execution/session-inbox/inbox.js";
1
2
  import type { SessionCheckpointMigration } from "#execution/session/checkpoint-migrations.js";
2
3
  import { type DurableSessionState } from "#execution/durable-session-store.js";
3
- import { type SessionOwnerActivation } from "#execution/session/handoff.js";
4
+ import { type SessionCheckpoint, type SessionOwnerActivation } from "#execution/session/handoff.js";
5
+ import { type SessionHandoffStart } from "#execution/session/entry-input.js";
4
6
  /** Parses retained work with this deployment's code before deciding whether it can move. */
5
7
  export declare function isSessionStateIdleForHandoff(input: {
6
8
  readonly sessionState: DurableSessionState;
7
9
  }): boolean;
8
- /** Reads durable work using the source deployment's handoff contract. */
9
- export declare function isSessionIdleForHandoffStep(input: {
10
- readonly sessionState: DurableSessionState;
10
+ export type SuccessorStartInput = SessionHandoffStart & {
11
+ readonly activationToken: string;
12
+ /** Original run whose stream stays the public session stream. */
13
+ readonly anchorRunId: string;
14
+ readonly attemptKey: string;
15
+ readonly checkpoint: SessionCheckpoint;
16
+ readonly hookTokens: readonly string[];
17
+ /** The owner starting the successor, which hands over what it accepts meanwhile. */
18
+ readonly sourceRunId: string;
19
+ readonly targetDeploymentId: string;
20
+ };
21
+ export type SuccessorStart = {
22
+ readonly kind: "not-idle";
23
+ } | {
24
+ readonly kind: "unsupported";
25
+ } | {
26
+ readonly kind: "started";
27
+ readonly runId: string;
28
+ };
29
+ /**
30
+ * Starts a successor owner bound to the original session stream, unless this
31
+ * deployment's code finds work in the checkpoint that cannot move yet, or the
32
+ * World cannot move session addresses between runs without releasing them.
33
+ */
34
+ export declare function startSuccessorStep(input: SuccessorStartInput): Promise<SuccessorStart>;
35
+ /** Whether `runId` has ended; a run not readable yet has not. */
36
+ export declare function hasRunEndedStep(input: {
37
+ readonly runId: string;
11
38
  }): Promise<boolean>;
12
39
  export type SessionCheckpointValidation = {
13
40
  readonly kind: "valid";
14
41
  } | {
15
42
  readonly kind: "incompatible";
16
43
  readonly reason: "checkpoint-version";
44
+ }
45
+ /** The source no longer holds every address it handed over, so none may be taken. */
46
+ | {
47
+ readonly kind: "source-gone";
17
48
  };
18
49
  /**
19
- * Validates an upgraded checkpoint and resolves the target deployment's
20
- * compiled bundle.
50
+ * Validates an upgraded checkpoint, resolves the target deployment's compiled
51
+ * bundle, and records this owner's identity before it claims any hook.
21
52
  *
22
53
  * A checkpoint this deployment cannot read is a settled answer, not a fault,
23
54
  * so it returns rather than throws: retrying the step can never change it.
24
55
  */
25
- export declare function validateSessionCheckpointStep(input: {
56
+ export declare function prepareSuccessorStep(input: {
57
+ /** Absent for version 1 sources, which predate the field. */
58
+ readonly handoffVersion: number | undefined;
26
59
  readonly migration: SessionCheckpointMigration;
27
60
  readonly sessionId: string;
61
+ /** Version 3: the run that still holds the session and the addresses it hands over. */
62
+ readonly source?: {
63
+ readonly runId: string;
64
+ readonly hookTokens: readonly string[];
65
+ };
28
66
  }): Promise<SessionCheckpointValidation>;
29
67
  /**
30
68
  * Stops idle subagent sessions an older owner kept resumable, after this owner
@@ -43,12 +81,12 @@ export declare function reportSessionHandoffRetainedStep(input: {
43
81
  readonly targetDeploymentId: string;
44
82
  }): Promise<void>;
45
83
  /**
46
- * Records the identity of an owner that this deployment's code did not start
47
- * (a handoff successor or a legacy importer) before it claims any hook:
48
- * ingress reads `$eve.version` to decide whether the owner can run, so a
49
- * missing value would strand a healthy session. The value comes from this
50
- * step's code, not the previous owner's. Unlike observability attributes, a
51
- * failed write fails the boot.
84
+ * Records the identity of a legacy importer before it claims any hook;
85
+ * handoff successors do the same inside `prepareSuccessorStep`. Ingress reads
86
+ * `$eve.version` to decide whether the owner can run, so a missing value would
87
+ * strand a healthy session. The value comes from this step's code, not the
88
+ * previous owner's. Unlike observability attributes, a failed write fails the
89
+ * boot.
52
90
  */
53
91
  export declare function recordSessionOwnerStep(input: {
54
92
  readonly sessionId: string;
@@ -57,6 +95,30 @@ export declare function signalSessionOwnerActivationStep(input: {
57
95
  readonly activation: SessionOwnerActivation;
58
96
  readonly token: string;
59
97
  }): Promise<void>;
98
+ /** Makes a takeover with no attempt open visible: every forced claim belongs to one. */
99
+ export declare function reportSessionTakenOverOutsideAttemptStep(input: {
100
+ readonly sessionId: string;
101
+ }): Promise<void>;
102
+ /**
103
+ * Makes a move whose handover part keeps failing visible; the owner keeps
104
+ * retrying, because dropping the part would lose an accepted message.
105
+ */
106
+ export declare function reportHandoverPartStuckStep(input: {
107
+ readonly attemptKey: string;
108
+ readonly error: string | undefined;
109
+ readonly failures: number;
110
+ readonly index: number;
111
+ readonly sessionId: string;
112
+ }): Promise<void>;
113
+ /**
114
+ * Sends one handover part through the stable session address, which reaches
115
+ * whichever run owns the session. A part that keeps failing fails the step,
116
+ * for the caller to retry until it is delivered.
117
+ */
118
+ export declare function sendSessionHandoverPartStep(input: {
119
+ readonly part: Omit<SessionHandoverPayload, "kind">;
120
+ readonly sessionId: string;
121
+ }): Promise<"delivered" | "session-ended">;
60
122
  export declare function signalSessionAnchorStep(input: {
61
123
  readonly result: {
62
124
  readonly output: unknown;
@@ -1 +1 @@
1
- import{openInputs,openSignIns}from"#protocol/session-projection.js";import{createLogger,logError}from"#internal/logging.js";import{resolveInstalledPackageInfo}from"#internal/application/package.js";import{walkCauseChain}from"#shared/errors.js";import{getSandboxEnvironmentRuntime}from"#shared/sandbox-environment.js";import{BundleKey}from"#runtime/sessions/runtime-context-keys.js";import{queuedInput,storedProjection}from"#harness/session-machine/view.js";import{cancelRun,getWorld,resumeHook}from"#internal/workflow/runtime.js";import{getResolvedRuntimeAgentNode}from"#runtime/graph.js";import{readDurableSession}from"#execution/durable-session-store.js";import{getBlockingWorkflowToolRuns}from"#harness/workflow-tool-runs.js";import{IncompatibleStateLayoutError,deserializeContext}from"#context/serialize.js";import{holdsHitlRequests}from"#harness/hitl/index.js";import{EntityConflictError,RunExpiredError,WorkflowRunNotFoundError}from"#compiled/@workflow/errors/index.js";import{EVE_SESSION_ATTRIBUTE,EVE_VERSION_ATTRIBUTE}from"#execution/eve-workflow-attributes.js";import{SESSION_CHECKPOINT_VERSION}from"#execution/session/handoff.js";const log=createLogger(`execution.handoff`);function isSessionStateIdleForHandoff(e){let{state:t}=readDurableSession(e.sessionState),n=getBlockingWorkflowToolRuns(t);if(holdsHitlRequests(t)||[`eve.runtime.pendingInputBatch`,`eve.runtime.pendingCoordinationBatch`,`eve.runtime.deferredStepInput`,`eve.harness.pendingWorkflowInterrupt`].some(e=>t?.[e]!==void 0))return!1;let r=t?.[`eve.runtime.pendingInputBatches`];if(r!==void 0&&(!Array.isArray(r)||r.length>0))return!1;let i=storedProjection(t);return n.length===0&&i.activeTurnId===void 0&&openInputs(i).length===0&&openSignIns(i).length===0&&queuedInput(t)===void 0}async function isSessionIdleForHandoffStep(e){"use step";return isSessionStateIdleForHandoff(e)}async function validateSessionCheckpointStep(e){"use step";let{migration:t}=e,unreadable=t=>(log.warn(`session handoff refused: this deployment cannot read the checkpoint`,{detail:t,readableCheckpointVersion:SESSION_CHECKPOINT_VERSION,sessionId:e.sessionId}),{kind:`incompatible`,reason:`checkpoint-version`});if(t.kind===`incompatible`)return unreadable(t.detail);let{checkpoint:n}=t,r=n.sessionTimeoutMs;if(r!==!1&&(typeof r!=`number`||!Number.isFinite(r)||r<0))throw Error(`Session checkpoint contains an invalid timeout duration.`);let i;try{i=await deserializeContext(n.serializedContext)}catch(e){if(e instanceof IncompatibleStateLayoutError)return unreadable(e.message);throw e}let a=i.require(BundleKey),o=readDurableSession(n.sessionState).sandboxState?.session;if(o!=null&&o.providerName!==void 0){let e=getResolvedRuntimeAgentNode(a.graph,a.nodeId).sandboxRegistry.sandbox.inheritance?.definition??getResolvedRuntimeAgentNode(a.graph,a.nodeId).sandboxRegistry.sandbox.definition;if(e.kind!==`independent`)throw Error(`Session checkpoint sandbox has no resolved provider.`);let t=getSandboxEnvironmentRuntime(e.environment);if(o.providerName!==t.providerName||o.stateProtocolVersion!==t.stateProtocolVersion)throw Error(`Session checkpoint sandbox provider state is incompatible.`)}if(!isSessionStateIdleForHandoff(n))throw Error(`Session checkpoint contains pending work and cannot be handed off.`);return{kind:`valid`}}async function stopUntrackedChildSessionsStep(e){"use step";let t=await getWorld();for(let n of e.runIds)if(n!==e.sessionId)try{await cancelRun(t,n,{cancelReason:`Session upgraded`})}catch(t){[...walkCauseChain(t)].some(e=>EntityConflictError.is(e)||RunExpiredError.is(e)||WorkflowRunNotFoundError.is(e))||logError(log,`could not stop a subagent session after handoff`,t,{childRunId:n,sessionId:e.sessionId})}}async function reportSessionHandoffRetainedStep(e){"use step";log.warn(`session handoff failed; the current owner keeps the session`,{...e,checkpointVersion:SESSION_CHECKPOINT_VERSION})}async function recordSessionOwnerStep(e){"use step";let{setAttributes:t}=await import(`#compiled/@workflow/core/index.js`);await t({[EVE_SESSION_ATTRIBUTE]:e.sessionId,[EVE_VERSION_ATTRIBUTE]:resolveInstalledPackageInfo().version},{allowReservedAttributes:!0})}async function signalSessionOwnerActivationStep(e){"use step";await resumeHook(e.token,e.activation)}async function signalSessionAnchorStep(e){"use step";await resumeHook(e.token,e.result)}export{isSessionIdleForHandoffStep,isSessionStateIdleForHandoff,recordSessionOwnerStep,reportSessionHandoffRetainedStep,signalSessionAnchorStep,signalSessionOwnerActivationStep,stopUntrackedChildSessionsStep,validateSessionCheckpointStep};
1
+ import{openInputs,openSignIns}from"#protocol/session-projection.js";import{createLogger,logError}from"#internal/logging.js";import{sessionCommandHookToken,sessionInboxHookToken}from"#execution/session-inbox/address.js";import{resolveInstalledPackageInfo}from"#internal/application/package.js";import{walkCauseChain}from"#shared/errors.js";import{getSandboxEnvironmentRuntime}from"#shared/sandbox-environment.js";import{BundleKey}from"#runtime/sessions/runtime-context-keys.js";import{queuedInput,storedProjection}from"#harness/session-machine/view.js";import{cancelRun,getRun,getWorld,resumeHook}from"#internal/workflow/runtime.js";import{hasRunEnded,startWorkflowOnDeployment,workflowEntryReference}from"#execution/workflow-runtime.js";import{getResolvedRuntimeAgentNode}from"#runtime/graph.js";import{readDurableSession}from"#execution/durable-session-store.js";import{getBlockingWorkflowToolRuns}from"#harness/workflow-tool-runs.js";import{IncompatibleStateLayoutError,deserializeContext}from"#context/serialize.js";import{holdsHitlRequests}from"#harness/hitl/index.js";import{EntityConflictError,HookNotFoundError,RunExpiredError,WorkflowRunNotFoundError}from"#compiled/@workflow/errors/index.js";import{findSessionHookHolder}from"#execution/session-inbox/owner.js";import{EVE_SESSION_ATTRIBUTE,EVE_VERSION_ATTRIBUTE}from"#execution/eve-workflow-attributes.js";import{SESSION_CHECKPOINT_VERSION}from"#execution/session/handoff.js";import{mapWithConcurrency}from"#shared/map-with-concurrency.js";import{SESSION_HANDOFF_VERSION}from"#execution/session/entry-input.js";const log=createLogger(`execution.handoff`);function isSessionStateIdleForHandoff(e){let{state:t}=readDurableSession(e.sessionState),n=getBlockingWorkflowToolRuns(t);if(holdsHitlRequests(t)||[`eve.runtime.pendingInputBatch`,`eve.runtime.pendingCoordinationBatch`,`eve.runtime.deferredStepInput`,`eve.harness.pendingWorkflowInterrupt`].some(e=>t?.[e]!==void 0))return!1;let r=t?.[`eve.runtime.pendingInputBatches`];if(r!==void 0&&(!Array.isArray(r)||r.length>0))return!1;let i=storedProjection(t);return n.length===0&&i.activeTurnId===void 0&&openInputs(i).length===0&&openSignIns(i).length===0&&queuedInput(t)===void 0}async function startSuccessorStep(e){"use step";let{activationToken:t,anchorRunId:n,checkpoint:r,targetDeploymentId:i,...a}=e,{capabilities:o}=await getWorld();if(o?.hookForceClaim!==!0||o.hookRetention?.active!==!0)return log.warn(`session handoff unavailable: the Workflow world cannot force-claim hooks`,{sessionId:n}),{kind:`unsupported`};if(!isSessionStateIdleForHandoff(r))return{kind:`not-idle`};let s={...a,activationToken:t,checkpoint:r,handoffVersion:SESSION_HANDOFF_VERSION,kind:`handoff`,ownerDeploymentId:i,sessionWritable:getRun(n).getWritable(),sessionId:n};return{kind:`started`,runId:(await startWorkflowOnDeployment(workflowEntryReference,[s],i,r.retention===void 0?void 0:{experimental_retention:r.retention})).runId}}async function hasRunEndedStep(e){"use step";return await hasRunEnded(e.runId)}async function prepareSuccessorStep(e){"use step";let{migration:t}=e,unreadable=t=>(log.warn(`session handoff refused: this deployment cannot read the checkpoint`,{detail:t,readableCheckpointVersion:SESSION_CHECKPOINT_VERSION,sessionId:e.sessionId}),{kind:`incompatible`,reason:`checkpoint-version`});if(t.kind===`incompatible`)return unreadable(t.detail);let{checkpoint:n}=t,r=n.sessionTimeoutMs;if(r!==!1&&(typeof r!=`number`||!Number.isFinite(r)||r<0))throw Error(`Session checkpoint contains an invalid timeout duration.`);let i;try{i=await deserializeContext(n.serializedContext)}catch(e){if(e instanceof IncompatibleStateLayoutError)return unreadable(e.message);throw e}let a=i.require(BundleKey),o=readDurableSession(n.sessionState).sandboxState?.session;if(o!=null&&o.providerName!==void 0){let e=getResolvedRuntimeAgentNode(a.graph,a.nodeId).sandboxRegistry.sandbox.inheritance?.definition??getResolvedRuntimeAgentNode(a.graph,a.nodeId).sandboxRegistry.sandbox.definition;if(e.kind!==`independent`)throw Error(`Session checkpoint sandbox has no resolved provider.`);let t=getSandboxEnvironmentRuntime(e.environment);if(o.providerName!==t.providerName||o.stateProtocolVersion!==t.stateProtocolVersion)throw Error(`Session checkpoint sandbox provider state is incompatible.`)}if(!isSessionStateIdleForHandoff(n))throw Error(`Session checkpoint contains pending work and cannot be handed off.`);return e.source!==void 0&&!await holdsEveryAddress(e.source)?{kind:`source-gone`}:(await recordSessionOwner(e.sessionId),(e.handoffVersion??1)<SESSION_HANDOFF_VERSION&&log.info(`session moved from an owner that releases its addresses first`,{handoffVersion:e.handoffVersion??1,sessionId:e.sessionId}),{kind:`valid`})}async function holdsEveryAddress(e){return(await mapWithConcurrency(e.hookTokens,16,e=>findSessionHookHolder(sessionInboxHookToken(e)))).every(t=>t?.runId===e.runId)}async function stopUntrackedChildSessionsStep(e){"use step";let t=await getWorld();for(let n of e.runIds)if(n!==e.sessionId)try{await cancelRun(t,n,{cancelReason:`Session upgraded`})}catch(t){[...walkCauseChain(t)].some(e=>EntityConflictError.is(e)||RunExpiredError.is(e)||WorkflowRunNotFoundError.is(e))||logError(log,`could not stop a subagent session after handoff`,t,{childRunId:n,sessionId:e.sessionId})}}async function reportSessionHandoffRetainedStep(e){"use step";log.warn(`session handoff failed; the current owner keeps the session`,{...e,checkpointVersion:SESSION_CHECKPOINT_VERSION})}async function recordSessionOwnerStep(e){"use step";await recordSessionOwner(e.sessionId)}async function recordSessionOwner(e){let{setAttributes:t}=await import(`#compiled/@workflow/core/index.js`);await t({[EVE_SESSION_ATTRIBUTE]:e,[EVE_VERSION_ATTRIBUTE]:resolveInstalledPackageInfo().version},{allowReservedAttributes:!0})}async function signalSessionOwnerActivationStep(e){"use step";try{await resumeHook(e.token,e.activation)}catch(e){if(!HookNotFoundError.is(e))throw e}}async function reportSessionTakenOverOutsideAttemptStep(e){"use step";log.error(`session taken over with no handoff attempt open; this run leaves without finalizing`,{sessionId:e.sessionId})}async function reportHandoverPartStuckStep(e){"use step";log.warn(`session handover part keeps failing; the previous owner keeps retrying`,e)}async function sendSessionHandoverPartStep(e){"use step";try{return await resumeHook(sessionInboxHookToken(sessionCommandHookToken(e.sessionId)),{...e.part,kind:`handover`}),`delivered`}catch(e){if(HookNotFoundError.is(e))return`session-ended`;throw e}}async function signalSessionAnchorStep(e){"use step";await resumeHook(e.token,e.result)}export{hasRunEndedStep,isSessionStateIdleForHandoff,prepareSuccessorStep,recordSessionOwnerStep,reportHandoverPartStuckStep,reportSessionHandoffRetainedStep,reportSessionTakenOverOutsideAttemptStep,sendSessionHandoverPartStep,signalSessionAnchorStep,signalSessionOwnerActivationStep,startSuccessorStep,stopUntrackedChildSessionsStep};
@@ -1,4 +1,4 @@
1
- import type { DeliverHookPayload, SessionCapabilities } from "#channel/types.js";
1
+ import type { SessionCapabilities } from "#channel/types.js";
2
2
  import type { DurableSessionState } from "#execution/durable-session-store.js";
3
3
  import type { HarnessModelMessage } from "#harness/messages.js";
4
4
  import type { SessionInboxHandle, SessionInboxPayload } from "#execution/session-inbox/inbox.js";
@@ -29,11 +29,20 @@ export type SessionOwnerActivation = {
29
29
  } | {
30
30
  readonly kind: "incompatible";
31
31
  readonly reason: "checkpoint-version";
32
+ /** Version 3: the successor run that gave this verdict. */
33
+ readonly runId?: string;
32
34
  /** Commands accepted by partial successor claims before it rejected the checkpoint. */
33
35
  readonly payloads: readonly SessionInboxPayload[];
34
36
  } | {
35
37
  readonly kind: "failed";
38
+ /**
39
+ * Version 3: the successor took some of the session's hooks before it
40
+ * failed. The owner takes them back and waits for their handover.
41
+ */
42
+ readonly claimedHooks?: boolean;
36
43
  readonly error: unknown;
44
+ /** Version 3: the successor run that gave this verdict. */
45
+ readonly runId?: string;
37
46
  /** Commands accepted by partial successor claims before startup failed. */
38
47
  readonly payloads: readonly SessionInboxPayload[];
39
48
  };
@@ -45,65 +54,98 @@ interface SessionHandoffInput {
45
54
  readonly sessionId: string;
46
55
  }
47
56
  export declare function sessionAnchorToken(sessionId: string): string;
57
+ /** Only one run per attempt can hold this; a successor that loses it exits untouched. */
58
+ export declare function handoffLeaseToken(attemptKey: string): string;
59
+ /**
60
+ * Measured from the lease's creation and far beyond any queue delay, so a
61
+ * duplicate candidate that boots late still finds the token taken.
62
+ */
63
+ export declare const HANDOFF_LEASE_RETENTION = "7d";
48
64
  type SessionTransferOutcome = {
49
65
  readonly kind: "transferred";
50
66
  } | {
51
67
  readonly kind: "retained";
52
68
  readonly reason: SessionRetainedReason;
53
69
  };
54
- type SessionRetainedReason = "same-deployment" | "missing-deployment" | "not-idle" | "busy" | "accepted-during-release" | "activation-failed" | "checkpoint-incompatible" | "known-incompatible";
70
+ type SessionRetainedReason = "same-deployment" | "missing-deployment" | "not-idle" | "busy" | "activation-failed" | "checkpoint-incompatible" | "known-incompatible" | "unsupported-world";
55
71
  /** A compaction handoff the owner owes; the successor keeps this deadline. */
56
72
  export interface CompactionHandoff {
57
73
  readonly sessionTimeoutDeadline: Date | undefined;
58
74
  }
59
75
  type SessionTransferState = Pick<SessionCheckpoint, "history" | "serializedContext" | "sessionState">;
76
+ /**
77
+ * Sends `payloads` as handover parts, one payload per part: each was accepted
78
+ * on its own, so no part exceeds what the World already took. Every part is
79
+ * retried with backoff until it is delivered or the session has ended: the
80
+ * receiver reads nothing until the final part lands, and dropping a part
81
+ * would lose a message the session accepted.
82
+ */
83
+ export declare function sendSessionHandover(input: {
84
+ readonly attemptKey: string;
85
+ readonly payloads: readonly SessionInboxPayload[];
86
+ readonly sessionId: string;
87
+ }): Promise<void>;
60
88
  /**
61
89
  * The sole boundary for moving an idle session to a successor run: on another
62
90
  * exact deployment when newer code accepts a delivery, or on this deployment
63
91
  * after compaction so no single run's event log grows with the session.
64
92
  * Constructed once per owner; each `try*` method attempts one transfer.
65
- * When the upstream atomic hook-handoff primitive lands, only this class changes.
66
93
  *
67
- * Skipping known-incompatible targets requires this owner run to execute the
68
- * `incompatible` activation branch below. Owners still on an older eve build
69
- * treat that signal as a generic failure and attempt handoff on every turn;
70
- * they still benefit from successors returning incompatibility without step
71
- * retries.
94
+ * The owner never releases its hooks. The successor force-claims every one of
95
+ * them, and the takeover itself is the success signal: the owner then hands
96
+ * over whatever it accepted meanwhile and exits. Until the attempt settles
97
+ * the owner runs no turn.
72
98
  */
73
99
  export declare class SessionHandoff {
74
100
  private readonly input;
75
101
  private anchor;
76
102
  /** Deployments that rejected this session's checkpoint during this owner run. */
77
103
  private readonly incompatibleTargetDeploymentIds;
104
+ /** Counts this run's attempts; replay repeats the same sequence. */
105
+ private attempts;
106
+ /** The World cannot force-claim hooks, so no attempt can ever succeed. */
107
+ private worldUnsupported;
78
108
  constructor(input: SessionHandoffInput);
79
109
  /**
80
- * Attempts to move the session to the selected delivery's deployment. When a
81
- * compaction handoff is due and that deployment is this one or unknown, the
82
- * session moves to a fresh run here instead. The outcome states whether the
83
- * successor activated or this owner retained the session. Candidate failures
84
- * recover locally rather than escaping.
110
+ * Attempts to move the session to the selected delivery's deployment. The
111
+ * outcome states whether the successor took over or this owner retained the
112
+ * session. Candidate failures recover locally rather than escaping.
85
113
  */
86
- tryTransfer(selection: TurnSelection, state: SessionTransferState, options?: {
87
- readonly compaction?: CompactionHandoff;
88
- }): Promise<SessionTransferOutcome>;
114
+ tryTransfer(selection: TurnSelection, state: SessionTransferState): Promise<SessionTransferOutcome>;
89
115
  /**
90
116
  * Attempts to move a session that compacted to a fresh run on this
91
- * deployment. Without a delivery the caller guarantees no input is waiting,
92
- * and the successor parks until the next one arrives. The successor keeps
93
- * the session's deadline.
117
+ * deployment. The caller guarantees no input is waiting; the successor
118
+ * parks until the next one arrives and keeps the session's deadline.
94
119
  */
95
- tryCompactionTransfer(state: SessionTransferState, compaction: CompactionHandoff & {
96
- readonly delivery?: DeliverHookPayload;
97
- }): Promise<SessionTransferOutcome>;
120
+ tryCompactionTransfer(state: SessionTransferState, compaction: CompactionHandoff): Promise<SessionTransferOutcome>;
98
121
  private transfer;
122
+ /**
123
+ * The session was taken over while no attempt was open. This run leaves as
124
+ * after a transfer, without finalizing the stream the new owner now writes.
125
+ */
126
+ adoptTakeover(): Promise<{
127
+ readonly kind: "transferred";
128
+ }>;
129
+ /**
130
+ * Claims the attempt's lease. `gaveUpRunId` is the candidate whose outcome
131
+ * the owner is acting on: holding the lease, it can no longer take over.
132
+ */
133
+ private fenceAttempt;
134
+ /** Forwards what this run accepted but never read to the run that took the session over. */
135
+ private handOver;
136
+ /**
137
+ * Takes back every address a candidate that gave up may hold. Forced claims
138
+ * replace this run's own hooks, so this holds whether or not a pump has
139
+ * observed the candidate's claim yet.
140
+ */
141
+ private holdEveryToken;
142
+ private recoverFromSilentCandidate;
143
+ /** Keeps the session after a candidate's verdict, taking back any hook it claimed. */
144
+ private recover;
145
+ private ensureAnchor;
99
146
  /** After a transfer, the original run parks until the final owner reports the session result. */
100
147
  awaitAnchoredResult(): Promise<WorkflowEntryResult>;
101
148
  disposeAnchor(): Promise<void>;
102
- /** Starts the candidate and waits for it to activate or fail. */
103
- private startAndActivate;
104
- /** Reclaims the exact hook set and replays payloads accepted while it was released. */
105
- private recover;
106
149
  private reportRetained;
107
- private ensureAnchor;
108
150
  }
109
151
  export {};
@@ -1 +1 @@
1
- import{isObject}from"#shared/guards.js";import{sessionHandoffMarkerToken}from"#execution/session-inbox/address.js";import{startSessionOwnerStep}from"#execution/workflow-runtime.js";import{createHook,getWorkflowMetadata}from"#compiled/@workflow/core/index.js";import{claimHookOwnership,disposeHook}from"#execution/hook-ownership.js";import{sessionHookTokens}from"#execution/session/hook-tokens.js";import{isSessionIdleForHandoffStep,reportSessionHandoffRetainedStep}from"#execution/session/handoff-steps.js";import{readAcceptedDeploymentId}from"#execution/session/accepted-deployment.js";const SESSION_CHECKPOINT_VERSION=15;function sessionAnchorToken(e){return`${e}:anchor`}var SessionHandoff=class{input;anchor;incompatibleTargetDeploymentIds=new Set;constructor(e){this.input=e}async tryTransfer(e,t,n={}){let{delivery:r}=e,i=readAcceptedDeploymentId(r);return(i===void 0||i===this.input.deploymentId)&&n.compaction!==void 0?e.handoffEligible?await this.tryCompactionTransfer(t,{...n.compaction,delivery:r}):{kind:`retained`,reason:`busy`}:i===void 0?{kind:`retained`,reason:`missing-deployment`}:i===this.input.deploymentId?{kind:`retained`,reason:`same-deployment`}:this.incompatibleTargetDeploymentIds.has(i)?{kind:`retained`,reason:`known-incompatible`}:e.handoffEligible?await this.transfer(t,i,{delivery:r}):{kind:`retained`,reason:`busy`}}async tryCompactionTransfer(e,t){let{deploymentId:n}=this.input;return this.incompatibleTargetDeploymentIds.has(n)?{kind:`retained`,reason:`known-incompatible`}:await this.transfer(e,n,{...t,reason:`compaction`})}async transfer(e,t,n){let{inbox:r}=this.input;if(!await isSessionIdleForHandoffStep({sessionState:e.sessionState}))return{kind:`retained`,reason:`not-idle`};let i={...this.input.checkpoint,...e,version:15},a=sessionHookTokens(e);await this.ensureAnchor();let o=a.map(e=>createHook({token:sessionHandoffMarkerToken(e)}));await Promise.all(o.map(e=>claimHookOwnership(e)));let s;try{let e=await r.release();if(e.length>0)return await this.recover(a,e),{kind:`retained`,reason:`accepted-during-release`};let o=[];try{let e=await this.startAndActivate(i,n,t);if(e.kind===`active`)return{kind:`transferred`};o=e.payloads,e.kind===`incompatible`?(this.incompatibleTargetDeploymentIds.add(t),s={reason:`checkpoint-incompatible`}):s={error:e.error,reason:`activation-failed`}}catch(e){s={error:e,reason:`activation-failed`}}await this.recover(a,o)}finally{await Promise.all(o.map(e=>disposeHook(e)))}return await this.reportRetained(s.reason,t,s.error),{kind:`retained`,reason:s.reason}}async awaitAnchoredResult(){if(this.anchor===void 0)throw Error(`Session anchor was never claimed.`);try{return await this.anchor}finally{await this.disposeAnchor()}}async disposeAnchor(){if(this.anchor===void 0)return;let e=this.anchor;this.anchor=void 0,await disposeHook(e)}async startAndActivate(e,t,n){let r=createHook({token:`${getWorkflowMetadata().workflowRunId}:handoff`});await claimHookOwnership(r);try{return await startSessionOwnerStep({...t,activationToken:r.token,anchorRunId:this.input.sessionId,checkpoint:e,targetDeploymentId:n}),await r}finally{await disposeHook(r)}}async recover(e,t){await this.input.inbox.claimSessionHooks(e),this.input.inbox.restore(t)}async reportRetained(e,t,n){let r=isObject(n)&&typeof n.message==`string`?n.message:void 0;await reportSessionHandoffRetainedStep({error:r,reason:e,sessionId:this.input.sessionId,sourceDeploymentId:this.input.deploymentId,targetDeploymentId:t})}async ensureAnchor(){this.input.isInitialOwner&&this.anchor===void 0&&(this.anchor=createHook({token:sessionAnchorToken(this.input.sessionId)}),await claimHookOwnership(this.anchor))}};export{SESSION_CHECKPOINT_VERSION,SessionHandoff,sessionAnchorToken};
1
+ import{isObject}from"#shared/guards.js";import{createHook,getWorkflowMetadata,sleep}from"#compiled/@workflow/core/index.js";import{claimHookOwnership,disposeHook,isHookConflictError}from"#execution/hook-ownership.js";import{hasRunEndedStep,reportHandoverPartStuckStep,reportSessionHandoffRetainedStep,reportSessionTakenOverOutsideAttemptStep,sendSessionHandoverPartStep,startSuccessorStep}from"#execution/session/handoff-steps.js";import{abandonHandoverIfRunEnds,watchRunEnd}from"#execution/session/run-watch.js";import{readAcceptedDeploymentId}from"#execution/session/accepted-deployment.js";const SESSION_CHECKPOINT_VERSION=15;function sessionAnchorToken(e){return`${e}:anchor`}function handoffLeaseToken(e){return`eve:handoff:lease:${e}`}const HANDOFF_LEASE_RETENTION=`7d`;function verdictRunId(e){return e.kind===`verdict`&&e.verdict.kind!==`active`?e.verdict.runId:void 0}async function sendSessionHandover(e){let{attemptKey:t,sessionId:n}=e,r=e.payloads.filter(e=>e.kind!==`handover`);for(let[e,i]of r.entries())if(await sendHandoverPart({attemptKey:t,index:e,payloads:[i]},n)===`session-ended`)return;await sendHandoverPart({attemptKey:t,final:!0,index:r.length,payloads:[]},n)}async function sendHandoverPart(e,t){for(let n=1,r=1;;n++)try{return await sendSessionHandoverPartStep({part:e,sessionId:t})}catch(i){n===HANDOVER_PART_FAILURES_BEFORE_REPORT&&await reportHandoverPartStuckStep({attemptKey:e.attemptKey,error:isObject(i)&&typeof i.message==`string`?i.message:void 0,failures:n,index:e.index,sessionId:t}).catch(()=>{}),await sleep(r*1e3),r=Math.min(r*2,60)}}const HANDOVER_PART_FAILURES_BEFORE_REPORT=5;var SessionHandoff=class{input;anchor;incompatibleTargetDeploymentIds=new Set;attempts=0;worldUnsupported=!1;constructor(e){this.input=e}async tryTransfer(e,t){let{delivery:n}=e,r=readAcceptedDeploymentId(n);return r===void 0?{kind:`retained`,reason:`missing-deployment`}:r===this.input.deploymentId?{kind:`retained`,reason:`same-deployment`}:this.incompatibleTargetDeploymentIds.has(r)?{kind:`retained`,reason:`known-incompatible`}:e.handoffEligible?await this.transfer(t,r,{delivery:n}):{kind:`retained`,reason:`busy`}}async tryCompactionTransfer(e,t){let{deploymentId:n}=this.input;return this.incompatibleTargetDeploymentIds.has(n)?{kind:`retained`,reason:`known-incompatible`}:await this.transfer(e,n,{...t,reason:`compaction`})}async transfer(e,t,n){let{inbox:r}=this.input;if(this.worldUnsupported)return{kind:`retained`,reason:`unsupported-world`};if(r.awaitingHandover)return{kind:`retained`,reason:`busy`};let i=getWorkflowMetadata().workflowRunId,a=this.attempts++,o=`${i}:${String(a)}`,s=r.claimedTokens,c=createHook({token:`${i}:handoff:${String(a)}`});this.ensureAnchor();let l=new AbortController,u=Date.now();try{let a;try{let r=await startSuccessorStep({...n,activationToken:c.token,anchorRunId:this.input.sessionId,attemptKey:o,checkpoint:{...this.input.checkpoint,...e,version:15},hookTokens:s,sourceRunId:i,targetDeploymentId:t});if(r.kind===`not-idle`)return{kind:`retained`,reason:`not-idle`};if(r.kind===`unsupported`)return this.worldUnsupported=!0,{kind:`retained`,reason:`unsupported-world`};a=r.runId}catch(e){let n=await this.fenceAttempt(o);if(n.kind!==`live`)return n.kind===`done`&&await this.holdEveryToken(s),await this.reportRetained(`activation-failed`,t,e),{kind:`retained`,reason:`activation-failed`};a=n.runId}let d=r.whenTakenOver(l.signal).then(()=>({kind:`taken-over`})),f,p;for(;;){if(f===void 0&&p===void 0&&r.hasPending()){let e=u+3e5-Date.now();p=sleep(Math.max(0,e)).then(()=>({kind:`deadline`}))}let e;try{e=await Promise.race([d,c.then(e=>({kind:`verdict`,verdict:e})),f===void 0?p??r.whenArrival().then(()=>({kind:`arrival`})):f.ended.then(()=>({kind:`candidate-ended`}))])}finally{f?.stop()}if(e.kind===`taken-over`)return await this.handOver(o);if(e.kind===`arrival`)continue;let n=e.kind===`candidate-ended`?a:verdictRunId(e),i=await this.fenceAttempt(o,n);if(i.kind===`live`){a=i.runId,f=watchRunEnd(a);continue}i.kind===`done`&&i.runId!==verdictRunId(e)&&await this.holdEveryToken(s);let l=e.kind===`verdict`?await this.recover(e.verdict,{attemptKey:o,candidateRunId:a,hookTokens:s,targetDeploymentId:t}):this.recoverFromSilentCandidate(e.kind);return await this.reportRetained(l.reason,t,l.error),{kind:`retained`,reason:l.reason}}}finally{l.abort(),await disposeHook(c)}}async adoptTakeover(){return await reportSessionTakenOverOutsideAttemptStep({sessionId:this.input.sessionId}),this.ensureAnchor(),{kind:`transferred`}}async fenceAttempt(e,t){let n=createHook({experimental_minRetention:`7d`,token:handoffLeaseToken(e)});try{await claimHookOwnership(n)}catch(e){if(!isHookConflictError(e)||typeof e.conflictingRunId!=`string`)throw e;let n=e.conflictingRunId;return n===t||await hasRunEndedStep({runId:n})?{kind:`done`,runId:n}:{kind:`live`,runId:n}}return{kind:`won`}}async handOver(e){return await sendSessionHandover({attemptKey:e,payloads:this.input.inbox.drain(),sessionId:this.input.sessionId}),{kind:`transferred`}}async holdEveryToken(e){await this.input.inbox.claimSessionHooks(e,{force:!0})}recoverFromSilentCandidate(e){return{error:Error(e===`deadline`?`The successor run did not report within 5 minutes.`:`The successor run ended before the handoff settled.`),reason:`activation-failed`}}async recover(e,t){let{inbox:n}=this.input;switch(e.kind){case`active`:return{error:Error(`Successor reported active without a takeover.`),reason:`activation-failed`};case`incompatible`:return this.incompatibleTargetDeploymentIds.add(t.targetDeploymentId),e.payloads.length>0&&n.restore(e.payloads),{reason:`checkpoint-incompatible`};case`failed`:return e.claimedHooks===!0?(await n.claimSessionHooks(t.hookTokens,{force:!0,handover:t.attemptKey}),abandonHandoverIfRunEnds(n,{attemptKey:t.attemptKey,sourceRunId:e.runId??t.candidateRunId})):isHookConflictError(e.error)&&this.incompatibleTargetDeploymentIds.add(t.targetDeploymentId),e.payloads.length>0&&n.restore(e.payloads),{error:e.error,reason:`activation-failed`}}}ensureAnchor(){this.input.isInitialOwner&&this.anchor===void 0&&(this.anchor=createHook({token:sessionAnchorToken(this.input.sessionId)}))}async awaitAnchoredResult(){if(this.anchor===void 0)throw Error(`Session anchor was never claimed.`);try{return await this.anchor}finally{await this.disposeAnchor()}}async disposeAnchor(){if(this.anchor===void 0)return;let e=this.anchor;this.anchor=void 0,await disposeHook(e)}async reportRetained(e,t,n){let r=isObject(n)&&typeof n.message==`string`?n.message:void 0;await reportSessionHandoffRetainedStep({error:r,reason:e,sessionId:this.input.sessionId,sourceDeploymentId:this.input.deploymentId,targetDeploymentId:t})}};export{HANDOFF_LEASE_RETENTION,SESSION_CHECKPOINT_VERSION,SessionHandoff,handoffLeaseToken,sendSessionHandover,sessionAnchorToken};
@@ -1 +1 @@
1
- import{STUB_CONTEXT_KEY}from"#tool-stubs/types.js";import{workingTasks}from"#execution/tasks/table.js";import{settleCancelledTurnStep}from"#execution/settle-cancelled-turn-step.js";import"#execution/session-inbox/inbox.js";import{signalSessionAnchorStep}from"#execution/session/handoff-steps.js";import{SessionInputQueue}from"#execution/session/input-queue.js";import{createTurnControl}from"#execution/session/turn-control.js";import{SessionHandoff,sessionAnchorToken}from"#execution/session/handoff.js";import{notifyCancelledTaskCallerStep,notifyTurnCallerStep,resolveInitialTurnCallerStep}from"#subagents/parent-notification.js";import{withStubPlayback}from"#execution/tool-stubs/playback.js";import{nextTurnDelivery}from"#execution/session/next-input.js";import{SessionExecution}from"#execution/session/turn.js";import{SessionStateCursor}from"#execution/session/state-cursor.js";import{cancelWorkingTasks,sessionTaskTable}from"#execution/tasks/session.js";import{finalizeSession}from"#execution/session/finalization.js";function createSafeOuterWorkflowError(){let e=Error(`Agent workflow failed. Inspect the private session trace for details.`);return e.name=`EveWorkflowFailure`,e}async function runPreparedSession(e,t){let n=e.serializedContext[STUB_CONTEXT_KEY];n!==void 0&&n.rootSessionId===void 0&&(e.serializedContext[STUB_CONTEXT_KEY]={...n,rootSessionId:e.sessionId});let r=new SessionStateCursor({history:e.history,inbox:t,sessionWritable:e.sessionWritable,serializedContext:e.serializedContext,sessionState:e.sessionState}),i={caller:e.caller,terminalEmitted:!1},a=new SessionHandoff({checkpoint:{capabilities:e.capabilities,retention:e.retention,sessionTimeoutMs:e.sessionTimeoutMs},deploymentId:e.deploymentId,inbox:t,isInitialOwner:e.anchor.kind===`self`,sessionId:e.sessionId}),o={output:``,isError:!0},s;try{return o=await withStubPlayback(n,e.sessionId,async()=>{try{s=await runSessionLoop(e,{cursor:r,handoff:a,inbox:t,progress:i})}finally{await t.dispose()}return s.kind===`transferred`?e.anchor.kind===`self`?(o=await a.awaitAnchoredResult(),o):{output:``}:(o=await finalizeSession(s.outcome,{caller:i.caller,cursor:r,sessionWritable:e.sessionWritable}),i.terminalEmitted=!0,o)}),o}catch(t){throw i.terminalEmitted||await finalizeSession({error:t,kind:`failed`,turnId:i.turnId},{caller:i.caller,cursor:r,sessionWritable:e.sessionWritable}),createSafeOuterWorkflowError()}finally{await t.dispose(),await reportResultToAnchor(e,o,a,s)}}async function failSession(e){let t;try{t=await resolveInitialTurnCallerStep({serializedContext:e.serializedContext})}catch{}throw await finalizeSession({error:e.error,kind:`failed`},{caller:t,cursor:{serializedContext:e.serializedContext,sessionState:e.sessionState},sessionWritable:e.sessionWritable}),createSafeOuterWorkflowError()}async function reportResultToAnchor(e,t,n,r){switch(e.anchor.kind){case`self`:await n.disposeAnchor(),await e.anchor.notify?.(t);return;case`successor`:if(r?.kind===`transferred`)return;await signalSessionAnchorStep({result:t,token:sessionAnchorToken(e.sessionId)});return}}async function runSessionLoop(e,t){let{cursor:n,handoff:r,inbox:i,progress:a}=t,o=new SessionInputQueue,s=new SessionExecution({capabilities:e.capabilities,cursor:n,inbox:i,queue:o,sessionId:e.sessionId}),c=e.sessionTimeoutControl,nextParkedActivity=async()=>{for(;;){let e=await nextTurnDelivery({cursor:n,hasWorkingTasks:()=>workingTasks(sessionTaskTable(n)).length>0,inbox:i,queue:o});if(e.kind===`workflow`){await s.handleWorkflowMessage(e.message);continue}if(e.kind===`cancel-working-tasks`){await cancelWorkingTasks(n,`turn_cancelled`);continue}return e}},l=e.initialTurnControl??(e.anchor.kind===`self`?createTurnControl():void 0),u=0,d=!1,runTurn=async e=>{let t=a.caller,n=l;l=void 0,a.turnId=`turn_${String(u++)}`;let r=await s.runTurn(e,{caller:t,control:n});return r.caller!==void 0&&(a.caller=r.caller),r.compacted===!0&&(d=!0),r},transferState=()=>({history:n.history,serializedContext:n.serializedContext,sessionState:n.sessionState}),dueCompactionHandoff=()=>d&&a.caller===void 0&&workingTasks(sessionTaskTable(n)).length===0?{sessionTimeoutDeadline:e.sessionTimeoutDeadline}:void 0,tryCompactionHandoff=async()=>{let e=dueCompactionHandoff();if(e===void 0||o.pendingCount>0||i.hasPending())return;let t=await r.tryCompactionTransfer(transferState(),e);return t.kind===`transferred`?t:void 0},runDeliveredTurn=async e=>{let t=await r.tryTransfer(e,transferState(),{compaction:dueCompactionHandoff()});return t.kind===`transferred`?t:(e.delivery.caller!==void 0&&(a.caller=e.delivery.caller),{action:await runTurn({delivery:e.delivery}),kind:`action`})},settleCancelledTurn=async e=>{let t=await n.advanceWithHistory(t=>settleCancelledTurnStep({...t,reportUsage:e}));return a.caller=void 0,t},awaitPrewarmedAction=async()=>{for(;;){let e=await nextParkedActivity();switch(e.kind){case`expired`:case`reset`:case`closed`:return{kind:`terminal`,outcome:{kind:`expired`}};case`clear`:case`compact`:continue;case`turn`:return await runDeliveredTurn(e);case`cancel-turn`:continue}}},runInitialAction=async()=>{switch(e.start.kind){case`first-message`:return await awaitPrewarmedAction();case`parked`:return{action:{kind:`park`},kind:`action`};case`turn`:{let{input:t}=e.start;return{action:await runTurn(t===void 0?void 0:{delivery:t}),kind:`action`}}}};try{let[t,n]=await Promise.allSettled([runInitialAction(),c?.start()]);if(n.status===`rejected`)throw n.reason;if(t.status===`rejected`)throw t.reason;let r=t.value;if(r.kind!==`action`)return r;let i=r.action;for(;;){if(i.kind===`done`)return{kind:`terminal`,outcome:{action:i,kind:`done`}};if(i.cancelled===!0){let{caller:t}=a,n=await settleCancelledTurn(t!==void 0);if(t!==void 0){let r={caller:t,sessionId:e.sessionId};await notifyCancelledTaskCallerStep(n.usage===void 0?r:{...r,usage:n.usage})}}else i.settled!==void 0&&(a.caller!==void 0&&await notifyTurnCallerStep({caller:a.caller,lifecycle:`parked`,sessionId:e.sessionId,settled:i.settled}),a.caller=void 0);let t=await tryCompactionHandoff();if(t!==void 0)return t;let n=await nextParkedActivity();switch(n.kind){case`expired`:case`reset`:case`closed`:return{kind:`terminal`,outcome:{kind:`expired`}};case`clear`:case`compact`:i=await runTurn({control:n.kind});continue;case`cancel-turn`:await s.cancelTurnWork(),await settleCancelledTurn(!1),i={...i,settled:void 0};continue;case`turn`:{let e=await runDeliveredTurn(n);if(e.kind!==`action`)return e;i=e.action;continue}}}}finally{l?.dispose(),await c?.dispose()}}export{failSession,runPreparedSession};
1
+ import{STUB_CONTEXT_KEY}from"#tool-stubs/types.js";import{workingTasks}from"#execution/tasks/table.js";import{settleCancelledTurnStep}from"#execution/settle-cancelled-turn-step.js";import"#execution/session-inbox/inbox.js";import{signalSessionAnchorStep}from"#execution/session/handoff-steps.js";import{SessionInputQueue}from"#execution/session/input-queue.js";import{createTurnControl}from"#execution/session/turn-control.js";import{SessionHandoff,sessionAnchorToken}from"#execution/session/handoff.js";import{notifyCancelledTaskCallerStep,notifyTurnCallerStep,resolveInitialTurnCallerStep}from"#subagents/parent-notification.js";import{withStubPlayback}from"#execution/tool-stubs/playback.js";import{nextTurnDelivery}from"#execution/session/next-input.js";import{SessionExecution}from"#execution/session/turn.js";import{SessionStateCursor}from"#execution/session/state-cursor.js";import{cancelWorkingTasks,sessionTaskTable}from"#execution/tasks/session.js";import{finalizeSession}from"#execution/session/finalization.js";function createSafeOuterWorkflowError(){let e=Error(`Agent workflow failed. Inspect the private session trace for details.`);return e.name=`EveWorkflowFailure`,e}async function runPreparedSession(e,t){let n=e.serializedContext[STUB_CONTEXT_KEY];n!==void 0&&n.rootSessionId===void 0&&(e.serializedContext[STUB_CONTEXT_KEY]={...n,rootSessionId:e.sessionId});let r=new SessionStateCursor({history:e.history,inbox:t,sessionWritable:e.sessionWritable,serializedContext:e.serializedContext,sessionState:e.sessionState}),i={caller:e.caller,terminalEmitted:!1},a=new SessionHandoff({checkpoint:{capabilities:e.capabilities,retention:e.retention,sessionTimeoutMs:e.sessionTimeoutMs},deploymentId:e.deploymentId,inbox:t,isInitialOwner:e.anchor.kind===`self`,sessionId:e.sessionId}),o={output:``,isError:!0},s;try{return o=await withStubPlayback(n,e.sessionId,async()=>{try{s=await runSessionLoop(e,{cursor:r,handoff:a,inbox:t,progress:i})}finally{await t.dispose()}return s.kind===`transferred`?e.anchor.kind===`self`?(o=await a.awaitAnchoredResult(),o):{output:``}:(o=await finalizeSession(s.outcome,{caller:i.caller,cursor:r,sessionWritable:e.sessionWritable}),i.terminalEmitted=!0,o)}),o}catch(t){throw i.terminalEmitted||await finalizeSession({error:t,kind:`failed`,turnId:i.turnId},{caller:i.caller,cursor:r,sessionWritable:e.sessionWritable}),createSafeOuterWorkflowError()}finally{await t.dispose(),await reportResultToAnchor(e,o,a,s)}}async function failSession(e){let t;try{t=await resolveInitialTurnCallerStep({serializedContext:e.serializedContext})}catch{}throw await finalizeSession({error:e.error,kind:`failed`},{caller:t,cursor:{serializedContext:e.serializedContext,sessionState:e.sessionState},sessionWritable:e.sessionWritable}),createSafeOuterWorkflowError()}async function reportResultToAnchor(e,t,n,r){switch(e.anchor.kind){case`self`:await n.disposeAnchor(),await e.anchor.notify?.(t);return;case`successor`:if(r?.kind===`transferred`)return;await signalSessionAnchorStep({result:t,token:sessionAnchorToken(e.sessionId)});return}}async function runSessionLoop(e,t){let{cursor:n,handoff:r,inbox:i,progress:a}=t,o=new SessionInputQueue,s=new SessionExecution({capabilities:e.capabilities,cursor:n,inbox:i,queue:o,sessionId:e.sessionId}),c=e.sessionTimeoutControl,nextParkedActivity=async()=>{for(;;){let e=await nextTurnDelivery({cursor:n,hasWorkingTasks:()=>workingTasks(sessionTaskTable(n)).length>0,inbox:i,queue:o});if(e.kind===`workflow`){await s.handleWorkflowMessage(e.message);continue}if(e.kind===`cancel-working-tasks`){await cancelWorkingTasks(n,`turn_cancelled`);continue}return e}},l=e.initialTurnControl??(e.anchor.kind===`self`?createTurnControl():void 0),u=0,d=!1,runTurn=async e=>{let t=a.caller,n=l;l=void 0,a.turnId=`turn_${String(u++)}`;let r=await s.runTurn(e,{caller:t,control:n});return r.caller!==void 0&&(a.caller=r.caller),r.compacted===!0&&(d=!0),r},transferState=()=>({history:n.history,serializedContext:n.serializedContext,sessionState:n.sessionState}),dueCompactionHandoff=()=>d&&a.caller===void 0&&workingTasks(sessionTaskTable(n)).length===0?{sessionTimeoutDeadline:e.sessionTimeoutDeadline}:void 0,tryCompactionHandoff=async()=>{let e=dueCompactionHandoff();if(e===void 0||o.pendingCount>0||i.hasPending())return;let t=await r.tryCompactionTransfer(transferState(),e);return t.kind===`transferred`?t:void 0},runDeliveredTurn=async e=>{let t=await r.tryTransfer(e,transferState());return t.kind===`transferred`?t:(e.delivery.caller!==void 0&&(a.caller=e.delivery.caller),{action:await runTurn({delivery:e.delivery}),kind:`action`})},settleCancelledTurn=async e=>{let t=await n.advanceWithHistory(t=>settleCancelledTurnStep({...t,reportUsage:e}));return a.caller=void 0,t},awaitPrewarmedAction=async()=>{for(;;){let e=await nextParkedActivity();switch(e.kind){case`closed`:return i.takenOver?await r.adoptTakeover():{kind:`terminal`,outcome:{kind:`expired`}};case`expired`:case`reset`:return{kind:`terminal`,outcome:{kind:`expired`}};case`clear`:case`compact`:continue;case`turn`:return await runDeliveredTurn(e);case`cancel-turn`:continue}}},runInitialAction=async()=>{switch(e.start.kind){case`first-message`:return await awaitPrewarmedAction();case`parked`:return{action:{kind:`park`},kind:`action`};case`turn`:{let{input:t}=e.start;return{action:await runTurn(t===void 0?void 0:{delivery:t}),kind:`action`}}}};try{let[t,n]=await Promise.allSettled([runInitialAction(),c?.start()]);if(n.status===`rejected`)throw n.reason;if(t.status===`rejected`)throw t.reason;let o=t.value;if(o.kind!==`action`)return o;let l=o.action;for(;;){if(l.kind===`done`)return{kind:`terminal`,outcome:{action:l,kind:`done`}};if(l.cancelled===!0){let{caller:t}=a,n=await settleCancelledTurn(t!==void 0);if(t!==void 0){let r={caller:t,sessionId:e.sessionId};await notifyCancelledTaskCallerStep(n.usage===void 0?r:{...r,usage:n.usage})}}else l.settled!==void 0&&(a.caller!==void 0&&await notifyTurnCallerStep({caller:a.caller,lifecycle:`parked`,sessionId:e.sessionId,settled:l.settled}),a.caller=void 0);let t=await tryCompactionHandoff();if(t!==void 0)return t;let n=await nextParkedActivity();switch(n.kind){case`closed`:return i.takenOver?await r.adoptTakeover():{kind:`terminal`,outcome:{kind:`expired`}};case`expired`:case`reset`:return{kind:`terminal`,outcome:{kind:`expired`}};case`clear`:case`compact`:l=await runTurn({control:n.kind});continue;case`cancel-turn`:await s.cancelTurnWork(),await settleCancelledTurn(!1),l={...l,settled:void 0};continue;case`turn`:{let e=await runDeliveredTurn(n);if(e.kind!==`action`)return e;l=e.action;continue}}}}finally{l?.dispose(),await c?.dispose()}}export{failSession,runPreparedSession};
@@ -0,0 +1,22 @@
1
+ import type { SessionInboxHandle } from "#execution/session-inbox/inbox.js";
2
+ export interface RunWatch {
3
+ /** Resolves once the run has ended; never settles once stopped. */
4
+ readonly ended: Promise<void>;
5
+ stop(): void;
6
+ }
7
+ /**
8
+ * Watches a counterpart run so a wait on it has a way out when that run is
9
+ * gone. Durable sleeps between checks keep it deterministic under replay;
10
+ * `stop()` ends it before its next check.
11
+ */
12
+ export declare function watchRunEnd(runId: string): RunWatch;
13
+ /**
14
+ * Opens the inbox's handover gate with nothing restored if `sourceRunId` ends
15
+ * before its handover arrives: an ended run can never send it. The watch
16
+ * starts only once a read waits on the gate, so a handover that arrives
17
+ * before it is needed costs nothing.
18
+ */
19
+ export declare function abandonHandoverIfRunEnds(inbox: SessionInboxHandle, handover: {
20
+ readonly attemptKey: string;
21
+ readonly sourceRunId: string;
22
+ }): void;
@@ -0,0 +1 @@
1
+ import{sleep}from"#compiled/@workflow/core/index.js";import{hasRunEndedStep}from"#execution/session/handoff-steps.js";function watchRunEnd(e){let t=!1;return{ended:(async()=>{for(;!t&&(await sleep(`10s`),!t);)if(await hasRunEndedStep({runId:e}).catch(()=>!1)&&!t)return;return await new Promise(()=>{})})(),stop(){t=!0}}}function abandonHandoverIfRunEnds(e,t){e.whenBlockedOnHandover().then(()=>{if(!e.awaitingHandover)return;let n=watchRunEnd(t.sourceRunId);e.whenHandoverSettled().then(()=>n.stop()),n.ended.then(()=>e.abandonHandover(t.attemptKey))})}export{abandonHandoverIfRunEnds,watchRunEnd};
@@ -21,5 +21,8 @@ export declare function sessionInboxHookToken(token: string): string;
21
21
  export declare function isSessionInboxHookToken(token: string): boolean;
22
22
  /** Logical address behind a physical session inbox hook token; `undefined` for any other hook. */
23
23
  export declare function logicalSessionInboxToken(token: string): string | undefined;
24
- /** Marker a releasing owner leaves so ingress can tell "handoff in progress" from "no session". */
24
+ /**
25
+ * Marker an owner older than handoff version 3 leaves while it releases its
26
+ * hooks, so ingress can tell "handoff in progress" from "no session".
27
+ */
25
28
  export declare function sessionHandoffMarkerToken(token: string): string;
@@ -1 +1 @@
1
- import{createLogger}from"#internal/logging.js";import{logicalSessionInboxToken,sessionCommandHookToken,sessionInboxHookToken}from"#execution/session-inbox/address.js";import{resolveInstalledPackageInfo}from"#internal/application/package.js";import{encodeMessageStreamEvent,stampMessageStreamEvent}from"#protocol/message.js";import{cancelRun,getRun,getWorld}from"#internal/workflow/runtime.js";import{isInactiveWorkflowRunError}from"#internal/workflow/is-inactive-workflow-run-error.js";import{logicalSessionToken,requireSessionId}from"#execution/session-inbox/resume.js";import{sessionFailed}from"#harness/session-machine/transitions.js";import{findSessionHookHolder}from"#execution/session-inbox/owner.js";import{EVE_SESSION_ATTRIBUTE,EVE_TYPE_ATTRIBUTE}from"#execution/eve-workflow-attributes.js";import{waitForSessionHooksRelease}from"#execution/session-inbox/hook-release.js";const log=createLogger(`execution.session-inbox`);async function endStrandedSession(e,t,n){let r=typeof t==`string`?await resolveStrandedSessionId(e):t.sessionId,{ownerRunId:i}=e,a=await getWorld();await assertStillServesSession(r,i);let o=await listOwnedSessionTokens(a,i);return await publishTerminalEvent(r,n),await cancelRuns(a,[i,r],`Session ended: its deployment was retired`),await waitForSessionHooksRelease([sessionCommandHookToken(r),logicalSessionToken(t),...o],i),log.warn(`Reset stranded session`,{currentEveVersion:resolveInstalledPackageInfo().version,previousEveVersion:e.eveVersion??`unknown`,previousSessionId:r,trigger:n}),r}async function assertStillServesSession(e,t){let n=await findSessionHookHolder(sessionInboxHookToken(sessionCommandHookToken(e)));if(n!==void 0&&n.runId!==t)throw Error(`Session "${e}" changed owners while it was being reset. Nothing was ended; retry the request.`)}async function publishTerminalEvent(e,t){let n=getRun(e).getWritable().getWriter(),r=(async()=>(await n.write(encodeMessageStreamEvent(stampMessageStreamEvent(sessionFailed({code:`session_stranded`,details:{trigger:t},message:`This session is no longer available.`,sessionId:e,usage:void 0})))),await n.close(),!0))();r.catch(()=>{});let i,a=new Promise(e=>{i=setTimeout(()=>e(!1),5e3)});try{await Promise.race([r,a])||log.warn(`Timed out finishing the stranded session stream`,{sessionId:e})}catch{log.warn(`Could not finish the stranded session stream`,{sessionId:e})}finally{clearTimeout(i),n.releaseLock()}}async function cancelRuns(e,t,n){for(let r of new Set(t))try{await cancelRun(e,r,{cancelReason:n})}catch(e){if(!isInactiveWorkflowRunError(e))throw e}}async function resolveStrandedSessionId(e){let{attributes:t,runId:n}=e.ownerRun,r=t[EVE_SESSION_ATTRIBUTE];if(r!==void 0)return r;if(e.hook!==void 0)try{return requireSessionId(await e.hook.metadata)}catch{}if(t[EVE_TYPE_ATTRIBUTE]!==void 0)return n;throw Error(`Cannot reset stranded session owner run "${n}": it does not record which session it serves. Cancel the run directly.`)}async function listOwnedSessionTokens(e,t){let n=[],r;for(let i=0;i<10;i++){let i=await e.hooks.list({pagination:{cursor:r,limit:100},resolveData:`none`,runId:t});for(let e of i.data){let t=logicalSessionInboxToken(e.token);t!==void 0&&n.push(t)}if(!i.hasMore||i.cursor===null)break;r=i.cursor}return n}export{endStrandedSession};
1
+ import{createLogger}from"#internal/logging.js";import{logicalSessionInboxToken,sessionCommandHookToken,sessionInboxHookToken}from"#execution/session-inbox/address.js";import{resolveInstalledPackageInfo}from"#internal/application/package.js";import{encodeMessageStreamEvent,stampMessageStreamEvent}from"#protocol/message.js";import{cancelRun,getRun,getWorld}from"#internal/workflow/runtime.js";import{isInactiveWorkflowRunError}from"#internal/workflow/is-inactive-workflow-run-error.js";import{logicalSessionToken,requireSessionId}from"#execution/session-inbox/resume.js";import{sessionFailed}from"#harness/session-machine/transitions.js";import{findSessionHookHolder}from"#execution/session-inbox/owner.js";import{EVE_SESSION_ATTRIBUTE,EVE_TYPE_ATTRIBUTE}from"#execution/eve-workflow-attributes.js";import{waitForSessionHooksRelease}from"#execution/session-inbox/hook-release.js";const log=createLogger(`execution.session-inbox`);async function endStrandedSession(e,t,n){let r=typeof t==`string`?await resolveStrandedSessionId(e):t.sessionId,{ownerRunId:i}=e,a=await getWorld();await assertStillServesSession(r,i);let o=await listOwnedSessionTokens(a,i);return await publishTerminalEvent(r,n),await cancelRuns(a,[i,r],`Session ended: its deployment was retired`),await waitForSessionHooksRelease([sessionCommandHookToken(r),logicalSessionToken(t),...o],{ownerRunId:i,sessionId:r}),log.warn(`Reset stranded session`,{currentEveVersion:resolveInstalledPackageInfo().version,previousEveVersion:e.eveVersion??`unknown`,previousSessionId:r,trigger:n}),r}async function assertStillServesSession(e,t){let n=await findSessionHookHolder(sessionInboxHookToken(sessionCommandHookToken(e)));if(n!==void 0&&n.runId!==t)throw Error(`Session "${e}" changed owners while it was being reset. Nothing was ended; retry the request.`)}async function publishTerminalEvent(e,t){let n=getRun(e).getWritable().getWriter(),r=(async()=>(await n.write(encodeMessageStreamEvent(stampMessageStreamEvent(sessionFailed({code:`session_stranded`,details:{trigger:t},message:`This session is no longer available.`,sessionId:e,usage:void 0})))),await n.close(),!0))();r.catch(()=>{});let i,a=new Promise(e=>{i=setTimeout(()=>e(!1),5e3)});try{await Promise.race([r,a])||log.warn(`Timed out finishing the stranded session stream`,{sessionId:e})}catch{log.warn(`Could not finish the stranded session stream`,{sessionId:e})}finally{clearTimeout(i),n.releaseLock()}}async function cancelRuns(e,t,n){for(let r of new Set(t))try{await cancelRun(e,r,{cancelReason:n})}catch(e){if(!isInactiveWorkflowRunError(e))throw e}}async function resolveStrandedSessionId(e){let{attributes:t,runId:n}=e.ownerRun,r=t[EVE_SESSION_ATTRIBUTE];if(r!==void 0)return r;if(e.hook!==void 0)try{return requireSessionId(await e.hook.metadata)}catch{}if(t[EVE_TYPE_ATTRIBUTE]!==void 0)return n;throw Error(`Cannot reset stranded session owner run "${n}": it does not record which session it serves. Cancel the run directly.`)}async function listOwnedSessionTokens(e,t){let n=[],r;for(let i=0;i<10;i++){let i=await e.hooks.list({pagination:{cursor:r,limit:100},resolveData:`none`,runId:t});for(let e of i.data){let t=logicalSessionInboxToken(e.token);t!==void 0&&n.push(t)}if(!i.hasMore||i.cursor===null)break;r=i.cursor}return n}export{endStrandedSession};
@@ -1,4 +1,11 @@
1
1
  /** Longest ingress waits for a session hook to change hands. */
2
2
  export declare const SESSION_HOOK_HANDOVER_TIMEOUT_MS = 30000;
3
- /** Waits until none of the logical session addresses belongs to `ownerRunId` any more. */
4
- export declare function waitForSessionHooksRelease(logicalTokens: Iterable<string>, ownerRunId: string): Promise<void>;
3
+ /**
4
+ * Waits until no run serving `session` holds any of the logical addresses. A
5
+ * handoff successor serves the same session, so only the session's end (or
6
+ * an unrelated run claiming the address afterwards) satisfies the wait.
7
+ */
8
+ export declare function waitForSessionHooksRelease(logicalTokens: Iterable<string>, session: {
9
+ readonly ownerRunId: string;
10
+ readonly sessionId: string;
11
+ }): Promise<void>;
@@ -1 +1 @@
1
- import{sessionInboxHookToken}from"#execution/session-inbox/address.js";import{findSessionHookHolder}from"#execution/session-inbox/owner.js";const SESSION_HOOK_HANDOVER_TIMEOUT_MS=3e4;async function waitForSessionHooksRelease(e,t){await Promise.all([...new Set(e)].map(e=>waitForHookRelease(sessionInboxHookToken(e),t)))}async function waitForHookRelease(e,t){let n=Date.now()+SESSION_HOOK_HANDOVER_TIMEOUT_MS;for(;;){if((await findSessionHookHolder(e))?.runId!==t)return;if(Date.now()>=n)throw Error(`Timed out waiting for session "${t}" to release inbox "${e}".`);await new Promise(e=>setTimeout(e,20))}}export{SESSION_HOOK_HANDOVER_TIMEOUT_MS,waitForSessionHooksRelease};
1
+ import{sessionInboxHookToken}from"#execution/session-inbox/address.js";import{requireSessionId}from"#execution/session-inbox/resume.js";import{findSessionHookHolder}from"#execution/session-inbox/owner.js";const SESSION_HOOK_HANDOVER_TIMEOUT_MS=3e4;async function waitForSessionHooksRelease(e,t){await Promise.all([...new Set(e)].map(e=>waitForHookRelease(sessionInboxHookToken(e),t)))}async function waitForHookRelease(e,t){let n=Date.now()+SESSION_HOOK_HANDOVER_TIMEOUT_MS;for(;;){if(!await servesSession(e,t))return;if(Date.now()>=n)throw Error(`Timed out waiting for session "${t.sessionId}" to release inbox "${e}".`);await new Promise(e=>setTimeout(e,20))}}async function servesSession(e,t){let n=await findSessionHookHolder(e);if(n===void 0)return!1;try{if(n.hook!==void 0)return requireSessionId(await n.hook.metadata)===t.sessionId}catch{}return n.runId===t.ownerRunId}export{SESSION_HOOK_HANDOVER_TIMEOUT_MS,waitForSessionHooksRelease};
@@ -11,12 +11,31 @@ export interface SessionFailurePayload {
11
11
  readonly kind: "session-failure";
12
12
  readonly error: string;
13
13
  }
14
- export type SessionInboxPayload = SessionFailurePayload | HookPayload | SessionCommand | WorkflowToolRunMessage | AuthorizationCallbackPayload;
14
+ /**
15
+ * One part of the previous owner's accepted-but-unread payloads after a
16
+ * handoff. The runtime sends these commands; channel request bodies cannot
17
+ * supply them.
18
+ */
19
+ export interface SessionHandoverPayload {
20
+ readonly kind: "handover";
21
+ /** The handoff attempt this answers; see {@link SessionHookBatchClaimOptions.handover}. */
22
+ readonly attemptKey: string;
23
+ /** Parts apply in index order; a retried send repeats its index. */
24
+ readonly index: number;
25
+ /** The last part; reads resume once every part before it has been applied. */
26
+ readonly final?: true;
27
+ readonly payloads: readonly Exclude<SessionInboxPayload, SessionHandoverPayload>[];
28
+ }
29
+ export type SessionInboxPayload = SessionFailurePayload | SessionHandoverPayload | HookPayload | SessionCommand | WorkflowToolRunMessage | AuthorizationCallbackPayload;
30
+ /**
31
+ * While a handover is awaited (see {@link SessionHookBatchClaimOptions.handover}),
32
+ * reads expose nothing; interrupt handlers still fire on arrival.
33
+ */
15
34
  export interface SessionInboxReader {
16
35
  /**
17
36
  * Waits for and removes the next accepted payload in arrival order, or
18
- * `undefined` once every hook is released. Only one consumer waits at a
19
- * time; the owner program is strictly sequential.
37
+ * `undefined` once no hook can deliver any more (released or taken over).
38
+ * Only one consumer waits at a time; the owner program is strictly sequential.
20
39
  */
21
40
  next(): Promise<SessionInboxPayload | undefined>;
22
41
  /** Removes every payload accepted so far, in arrival order. */
@@ -33,21 +52,62 @@ export interface SessionInboxReader {
33
52
  * consumer still processes it in order.
34
53
  */
35
54
  onInterrupt(handler: (payload: SessionInboxPayload) => void): () => void;
36
- /** Observes deliveries without consuming them; replays unread deliveries on subscription. */
55
+ /**
56
+ * Observes readable deliveries without consuming them; replays unread
57
+ * deliveries on subscription and when a handover arrives, so handlers must
58
+ * be idempotent.
59
+ */
37
60
  onDelivery(handler: (payload: SessionInboxPayload) => void): () => void;
38
61
  /** Requeues payloads a successor handed back, ahead of everything pumped since. */
39
62
  restore(payloads: readonly SessionInboxPayload[]): void;
40
63
  }
64
+ export interface SessionHookClaimOptions {
65
+ /**
66
+ * Take the address over from the run that holds it instead of conflicting.
67
+ * Still rejects with `HookConflictError` when the holder's runtime cannot be
68
+ * taken from; throws `FatalError` when the World does not support it. A
69
+ * token this inbox already holds is claimed again on a fresh hook.
70
+ */
71
+ readonly force?: boolean;
72
+ }
73
+ export interface SessionHookBatchClaimOptions extends SessionHookClaimOptions {
74
+ /**
75
+ * Expect a `handover` for this attempt key. Before any hook is created,
76
+ * reads stop exposing anything; as its parts arrive on any hook, their
77
+ * payloads take their place after what was queued at this call and ahead of
78
+ * everything accepted since. Reads resume after the final part.
79
+ */
80
+ readonly handover?: string;
81
+ }
41
82
  export interface SessionInboxOwnership {
42
83
  /** Logical tokens currently claimed, in claim order. */
43
84
  readonly claimedTokens: readonly string[];
44
- claimSessionHook(token: string): Promise<void>;
85
+ claimSessionHook(token: string, options?: SessionHookClaimOptions): Promise<void>;
45
86
  /** Registers every token as one batch; all claims settle before the first failure propagates. */
46
- claimSessionHooks(tokens: readonly string[]): Promise<void>;
87
+ claimSessionHooks(tokens: readonly string[], options?: SessionHookBatchClaimOptions): Promise<void>;
47
88
  }
48
89
  export interface SessionInbox extends SessionInboxReader, SessionInboxOwnership {
49
90
  }
50
91
  export interface SessionInboxHandle extends SessionInbox {
92
+ /** A handover is expected and has not arrived; the session must not move again until it does. */
93
+ readonly awaitingHandover: boolean;
94
+ /** Resolves once no handover is awaited: it arrived, was abandoned, or the inbox closed. */
95
+ whenHandoverSettled(): Promise<void>;
96
+ /** Stops waiting for `attemptKey`'s handover, which can no longer arrive; queued payloads become readable. */
97
+ abandonHandover(attemptKey: string): void;
98
+ /** Resolves the first time a read waits on an awaited handover. */
99
+ whenBlockedOnHandover(): Promise<void>;
100
+ /** Resolves when the next payload arrives on any hook, readable or not. */
101
+ whenArrival(): Promise<void>;
102
+ /** Another run force-claimed every hook this inbox held. */
103
+ readonly takenOver: boolean;
104
+ /**
105
+ * Resolves once another run has force-claimed every hook this inbox held.
106
+ * Every payload delivered before the takeover is queued by then. Rejects if
107
+ * a reader fails, if the inbox is released or disposed first, or once
108
+ * `signal` aborts.
109
+ */
110
+ whenTakenOver(signal?: AbortSignal): Promise<void>;
51
111
  dispose(): Promise<void>;
52
112
  /** Disposes every hook and returns each payload the hooks accepted but the owner never read. */
53
113
  release(): Promise<SessionInboxPayload[]>;
@@ -1 +1 @@
1
- import{sessionInboxHookToken}from"#execution/session-inbox/address.js";import{createHook,getWorkflowMetadata}from"#compiled/@workflow/core/index.js";import{claimHookOwnership,disposeHook}from"#execution/hook-ownership.js";import{releaseSessionHooksStep}from"#execution/session-inbox/release-step.js";function isInterrupt(e){return e.kind===`cancel`||e.kind===`reset`||e.kind===`session-timeout`||e.kind===`session-failure`}function createSessionInbox(e){let t=[],n=[],r=new Set,i=new Set,a=new Set,o,notify=()=>{for(let e of r)e();r.clear()},wait=()=>new Promise(e=>r.add(e)),closed=()=>t.every(e=>e.closed||e.stopping),pump=async e=>{let t=e.hook[Symbol.asyncIterator]();try{for(;!e.stopping;){let e=await t.next();if(e.done)break;let r=e.value;if(r.kind===`workflow-batch`){n.push(...r.messages),notify();continue}if(n.push(r),r.kind===`send`||r.kind===`deliver`)for(let e of a)e(r);if(isInterrupt(r))for(let e of i)e(r);notify()}}catch(t){e.stopping||(o={error:t})}finally{e.closed=!0,notify()}},stop=async()=>{let e=t.splice(0);for(let t of e)t.stopping=!0;notify(),await Promise.all(e.map(({hook:e})=>disposeHook(e)));let r=n.splice(0);return notify(),r},claimSessionHook=async n=>{if(!n)throw Error(`A session alias requires a nonempty continuation token.`);let r=t.find(e=>e.token===n);if(r!==void 0)return await r.registered;if(t.length>=256)throw Error(`A session may claim at most 256 addresses.`);let i={token:n,hook:createHook({token:sessionInboxHookToken(n),metadata:{sessionId:e}}),stopping:!1,closed:!1};t.push(i);try{i.registered=claimHookOwnership(i.hook),await i.registered,pump(i)}catch(e){throw t.splice(t.indexOf(i),1),e}};return{get claimedTokens(){return t.map(({token:e})=>e)},claimSessionHook,async claimSessionHooks(e){let t=await Promise.allSettled([...new Set(e)].map(claimSessionHook));for(let e of t)if(e.status===`rejected`)throw e.reason},async next(){for(;;){if(o!==void 0)throw o.error;if(n.length>0)return n.shift();if(closed())return;await wait()}},drain(){if(o!==void 0)throw o.error;return n.splice(0)},takeLeading(e){if(o!==void 0)throw o.error;let t=[];for(let r=n[0];r!==void 0&&e(r);r=n[0])t.push(r),n.shift();return t},hasPending(){if(o!==void 0)throw o.error;return n.length>0},async whenPending(){for(;o===void 0&&n.length===0&&!closed();)await wait()},onInterrupt(e){return i.add(e),()=>i.delete(e)},onDelivery(e){a.add(e);for(let t of n)(t.kind===`send`||t.kind===`deliver`)&&e(t);return()=>a.delete(e)},restore(e){if(t.length===0)throw Error(`Cannot restore session commands before reclaiming the session hooks.`);n.unshift(...e),notify()},async dispose(){await stop()},async release(){return t.length>0&&await releaseSessionHooksStep({ownerRunId:getWorkflowMetadata().workflowRunId,tokens:t.map(({hook:e})=>e.token)}),await stop()}}}function isWorkflowMessage(e){return e.kind===`agent-started`||e.kind===`started`||e.kind===`report`||e.kind===`reply`||e.kind===`request`||e.kind===`withdraw`||e.kind===`usage`||e.kind===`outcome`}export{createSessionInbox,isInterrupt,isWorkflowMessage};
1
+ import{sessionInboxHookToken}from"#execution/session-inbox/address.js";import{createHook,getWorkflowMetadata}from"#compiled/@workflow/core/index.js";import{claimHookOwnership,disposeHook,isHookForceClaimedError}from"#execution/hook-ownership.js";import{releaseSessionHooksStep}from"#execution/session-inbox/release-step.js";function isInterrupt(e){return e.kind===`cancel`||e.kind===`reset`||e.kind===`session-timeout`||e.kind===`session-failure`}function isDelivery(e){return e.kind===`send`||e.kind===`deliver`}function createSessionInbox(e){let t=[],n=[],r=new Set,i=new Set,a=new Set,o,s,c=new Set,l=new Set,u=!1,d=!1,notify=()=>{for(let e of r)e();r.clear()},wait=()=>new Promise(e=>r.add(e)),closed=()=>t.every(e=>e.closed||e.stopping),gated=()=>s!==void 0&&t.length>0,hasReadable=()=>!gated()&&n.length>0,throwIfFailed=()=>{if(o!==void 0)throw o.error},observe=(e,t,n)=>{for(let r of t)if(n(r))for(let t of e)t(r)},openHandover=()=>{s=void 0,observe(a,n,isDelivery),notify()},blockedOnHandover=()=>{if(gated()){for(let e of c)e();c.clear()}},acceptHandover=e=>{let t=s;if(t===void 0||e.attemptKey!==t.attemptKey||e.index<t.nextPart||t.parts.has(e.index))return;t.parts.set(e.index,e);let r=[];for(;;){let e=t.parts.get(t.nextPart);if(e===void 0)break;if(e.final===!0){observe(i,r,isInterrupt),openHandover();return}t.parts.delete(t.nextPart),t.nextPart+=1,n.splice(t.index,0,...e.payloads),t.index+=e.payloads.length,r.push(...e.payloads)}observe(i,r,isInterrupt),notify()},pump=async e=>{let r=e.hook[Symbol.asyncIterator]();try{for(;!e.stopping;){let e=await r.next();if(e.done)break;let t=e.value;for(let e of l)e();if(l.clear(),t.kind===`workflow-batch`){n.push(...t.messages),notify();continue}if(t.kind===`handover`){acceptHandover(t);continue}n.push(t),gated()||observe(a,[t],isDelivery),observe(i,[t],isInterrupt),notify()}}catch(n){if(e.stopping||e.replaced)return;isHookForceClaimedError(n)?(t.splice(t.indexOf(e),1),u=!0):o={error:n}}finally{e.closed=!0,notify()}},stop=async()=>{let e=t.splice(0);s=void 0,u=!1,d=!0;for(let t of e)t.stopping=!0;notify(),await Promise.all(e.map(({hook:e})=>disposeHook(e)));let r=n.splice(0);return notify(),r},claimSessionHook=async(n,r={})=>{if(!n)throw Error(`A session alias requires a nonempty continuation token.`);let i=r.force===!0,a=t.find(e=>e.token===n);if(a!==void 0&&!i)return await a.registered;if(a===void 0&&t.length>=256)throw Error(`A session may claim at most 256 addresses.`);let o={token:n,hook:createHook({token:sessionInboxHookToken(n),metadata:{sessionId:e},...i&&{experimental_force:!0}}),stopping:!1,closed:!1,replaced:!1};a===void 0?t.push(o):(a.replaced=!0,t.splice(t.indexOf(a),1,o)),d=!1;try{o.registered=claimHookOwnership(o.hook),await o.registered,u=!1,pump(o)}catch(e){throw a===void 0||a.closed?t.splice(t.indexOf(o),1):(a.replaced=!1,t.splice(t.indexOf(o),1,a)),e}};return{get claimedTokens(){return t.map(({token:e})=>e)},get awaitingHandover(){return s!==void 0},async whenHandoverSettled(){for(;s!==void 0;)await wait()},abandonHandover(e){s?.attemptKey===e&&openHandover()},whenBlockedOnHandover(){return new Promise(e=>c.add(e))},whenArrival(){return new Promise(e=>l.add(e))},get takenOver(){return u&&t.length===0},claimSessionHook,async claimSessionHooks(e,t={}){t.handover!==void 0&&(s={attemptKey:t.handover,index:n.length,nextPart:0,parts:new Map});let r=await Promise.allSettled([...new Set(e)].map(e=>claimSessionHook(e,t)));for(let e of r)if(e.status===`rejected`)throw e.reason},async whenTakenOver(e){for(e?.addEventListener(`abort`,notify,{once:!0});t.length>0||!u;){if(throwIfFailed(),d)throw Error(`The session inbox was released before a takeover.`);if(e?.aborted===!0)throw e.reason;await wait()}},async next(){for(;;){if(throwIfFailed(),hasReadable())return n.shift();if(closed())return;blockedOnHandover(),await wait()}},drain(){return throwIfFailed(),gated()?[]:n.splice(0)},takeLeading(e){throwIfFailed();let t=[];if(gated())return t;for(let r=n[0];r!==void 0&&e(r);r=n[0])t.push(r),n.shift();return t},hasPending(){return throwIfFailed(),hasReadable()},async whenPending(){for(;o===void 0&&!hasReadable()&&!closed();)blockedOnHandover(),await wait()},onInterrupt(e){return i.add(e),()=>i.delete(e)},onDelivery(e){if(a.add(e),!gated())for(let t of n)isDelivery(t)&&e(t);return()=>a.delete(e)},restore(e){if(t.length===0)throw Error(`Cannot restore session commands before reclaiming the session hooks.`);n.unshift(...e),s!==void 0&&(s.index+=e.length),notify()},async dispose(){await stop()},async release(){return t.length>0&&await releaseSessionHooksStep({ownerRunId:getWorkflowMetadata().workflowRunId,tokens:t.map(({hook:e})=>e.token)}),await stop()}}}function isWorkflowMessage(e){return e.kind===`agent-started`||e.kind===`started`||e.kind===`report`||e.kind===`reply`||e.kind===`request`||e.kind===`withdraw`||e.kind===`usage`||e.kind===`outcome`}export{createSessionInbox,isInterrupt,isWorkflowMessage};
@@ -7,9 +7,10 @@ export interface ResumedSessionInboxHook {
7
7
  readonly sessionId: Promise<string>;
8
8
  }
9
9
  /**
10
- * Resumes the current owner with the current command shape. During a
11
- * deployment handoff the address is briefly unowned; the releasing owner
12
- * leaves a marker for that interval, so delivery retries instead of
10
+ * Resumes the current owner with the current command shape. Owners on
11
+ * handoff version 3 never leave an address unowned. An older owner releases
12
+ * its hooks before its successor claims them and leaves a marker for that
13
+ * interval, so delivery retries instead of
13
14
  * reporting the session gone and letting a channel start a replacement.
14
15
  * The retry window bounds this request, not the handoff: a marker that
15
16
  * outlives it throws {@link SessionHandoffPendingError}.
@@ -1 +1 @@
1
- import{decide}from"#ai/decide.js";async function runAgentRouterTask(e,t){"use workflow";let n=await chooseTarget(e.message,descriptions(t),t.abortSignal),{message:r,status:i}=await(await t.agent(n).send(e.message,{signal:t.abortSignal})).result();if(i===`failed`)throw Error(`Agent "${n}" failed to handle the task.`);return r??null}async function chooseTarget(e,t,n){"use step";let r=Object.keys(t);if(r.length===0)throw Error(`agentRouter requires at least one available agent with a description.`);return r.length===1?r[0]:(await decide({abortSignal:n,state:{message:e},questions:{route:{type:`choice`,instructions:`Which subagent should handle this task?`,criteria:t}}})).answers.route.choice}function descriptions(e){return Object.fromEntries(Object.entries(e.agents).flatMap(([e,t])=>{let n=t.description.trim();return n.length===0?[]:[[e,n]]}))}runAgentRouterTask.workflowId=`workflow//eve@0.81.0//runAgentRouterTask`;export{runAgentRouterTask};
1
+ import{decide}from"#ai/decide.js";async function runAgentRouterTask(e,t){"use workflow";let n=await chooseTarget(e.message,descriptions(t),t.abortSignal),{message:r,status:i}=await(await t.agent(n).send(e.message,{signal:t.abortSignal})).result();if(i===`failed`)throw Error(`Agent "${n}" failed to handle the task.`);return r??null}async function chooseTarget(e,t,n){"use step";let r=Object.keys(t);if(r.length===0)throw Error(`agentRouter requires at least one available agent with a description.`);return r.length===1?r[0]:(await decide({abortSignal:n,state:{message:e},questions:{route:{type:`choice`,instructions:`Which subagent should handle this task?`,criteria:t}}})).answers.route.choice}function descriptions(e){return Object.fromEntries(Object.entries(e.agents).flatMap(([e,t])=>{let n=t.description.trim();return n.length===0?[]:[[e,n]]}))}runAgentRouterTask.workflowId=`workflow//eve@0.81.1//runAgentRouterTask`;export{runAgentRouterTask};