@cotal-ai/core 0.85.0 → 0.86.0

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 (153) hide show
  1. package/dist/agent-file.d.ts.map +1 -1
  2. package/dist/agent-file.js +10 -3
  3. package/dist/agent-file.js.map +1 -1
  4. package/dist/base64url.d.ts +5 -0
  5. package/dist/base64url.d.ts.map +1 -0
  6. package/dist/base64url.js +14 -0
  7. package/dist/base64url.js.map +1 -0
  8. package/dist/browser-control.d.ts +97 -0
  9. package/dist/browser-control.d.ts.map +1 -0
  10. package/dist/browser-control.js +66 -0
  11. package/dist/browser-control.js.map +1 -0
  12. package/dist/browser-validators-build.d.ts +23 -0
  13. package/dist/browser-validators-build.d.ts.map +1 -0
  14. package/dist/browser-validators-build.js +149 -0
  15. package/dist/browser-validators-build.js.map +1 -0
  16. package/dist/canonical-json.d.ts +14 -0
  17. package/dist/canonical-json.d.ts.map +1 -0
  18. package/dist/canonical-json.js +121 -0
  19. package/dist/canonical-json.js.map +1 -0
  20. package/dist/canonical.d.ts +2 -10
  21. package/dist/canonical.d.ts.map +1 -1
  22. package/dist/canonical.js +2 -108
  23. package/dist/canonical.js.map +1 -1
  24. package/dist/checkpoint-answer.d.ts.map +1 -1
  25. package/dist/checkpoint-answer.js +3 -4
  26. package/dist/checkpoint-answer.js.map +1 -1
  27. package/dist/connector-setup.d.ts +10 -1
  28. package/dist/connector-setup.d.ts.map +1 -1
  29. package/dist/digest-id.d.ts +4 -0
  30. package/dist/digest-id.d.ts.map +1 -0
  31. package/dist/digest-id.js +7 -0
  32. package/dist/digest-id.js.map +1 -0
  33. package/dist/endpoint-action.d.ts.map +1 -1
  34. package/dist/endpoint-action.js +2 -16
  35. package/dist/endpoint-action.js.map +1 -1
  36. package/dist/endpoint-auth-digest.d.ts +7 -0
  37. package/dist/endpoint-auth-digest.d.ts.map +1 -0
  38. package/dist/endpoint-auth-digest.js +13 -0
  39. package/dist/endpoint-auth-digest.js.map +1 -0
  40. package/dist/endpoint-call.d.ts +176 -0
  41. package/dist/endpoint-call.d.ts.map +1 -0
  42. package/dist/endpoint-call.js +490 -0
  43. package/dist/endpoint-call.js.map +1 -0
  44. package/dist/endpoint-caller.d.ts +113 -0
  45. package/dist/endpoint-caller.d.ts.map +1 -0
  46. package/dist/endpoint-caller.js +260 -0
  47. package/dist/endpoint-caller.js.map +1 -0
  48. package/dist/endpoint-checkpoint.d.ts.map +1 -1
  49. package/dist/endpoint-checkpoint.js +5 -14
  50. package/dist/endpoint-checkpoint.js.map +1 -1
  51. package/dist/endpoint-envelope.d.ts +0 -6
  52. package/dist/endpoint-envelope.d.ts.map +1 -1
  53. package/dist/endpoint-envelope.js +17 -18
  54. package/dist/endpoint-envelope.js.map +1 -1
  55. package/dist/endpoint-guard.d.ts.map +1 -1
  56. package/dist/endpoint-guard.js +2 -10
  57. package/dist/endpoint-guard.js.map +1 -1
  58. package/dist/endpoint-invoke.d.ts +36 -119
  59. package/dist/endpoint-invoke.d.ts.map +1 -1
  60. package/dist/endpoint-invoke.js +56 -300
  61. package/dist/endpoint-invoke.js.map +1 -1
  62. package/dist/endpoint-publish-denial.d.ts +1 -1
  63. package/dist/endpoint-publish-denial.d.ts.map +1 -1
  64. package/dist/endpoint-publish-denial.js +1 -1
  65. package/dist/endpoint-publish-denial.js.map +1 -1
  66. package/dist/endpoint-receipt.d.ts.map +1 -1
  67. package/dist/endpoint-receipt.js +2 -16
  68. package/dist/endpoint-receipt.js.map +1 -1
  69. package/dist/endpoint-subjects.d.ts.map +1 -1
  70. package/dist/endpoint-subjects.js.map +1 -1
  71. package/dist/endpoint-traits.d.ts.map +1 -1
  72. package/dist/endpoint-traits.js +3 -20
  73. package/dist/endpoint-traits.js.map +1 -1
  74. package/dist/endpoint-verbs.d.ts +18 -151
  75. package/dist/endpoint-verbs.d.ts.map +1 -1
  76. package/dist/endpoint-verbs.js +4 -500
  77. package/dist/endpoint-verbs.js.map +1 -1
  78. package/dist/endpoint-work.d.ts.map +1 -1
  79. package/dist/endpoint-work.js +2 -17
  80. package/dist/endpoint-work.js.map +1 -1
  81. package/dist/endpoint.d.ts +39 -15
  82. package/dist/endpoint.d.ts.map +1 -1
  83. package/dist/endpoint.js +90 -42
  84. package/dist/endpoint.js.map +1 -1
  85. package/dist/index.d.ts +4 -0
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +4 -0
  88. package/dist/index.js.map +1 -1
  89. package/dist/issued-accepted.d.ts +27 -0
  90. package/dist/issued-accepted.d.ts.map +1 -0
  91. package/dist/issued-accepted.js +62 -0
  92. package/dist/issued-accepted.js.map +1 -0
  93. package/dist/issued-authority.d.ts +3 -18
  94. package/dist/issued-authority.d.ts.map +1 -1
  95. package/dist/issued-authority.js +4 -55
  96. package/dist/issued-authority.js.map +1 -1
  97. package/dist/lifecycle-state.d.ts +3 -0
  98. package/dist/lifecycle-state.d.ts.map +1 -1
  99. package/dist/lifecycle-state.js +7 -0
  100. package/dist/lifecycle-state.js.map +1 -1
  101. package/dist/manager-caller.d.ts.map +1 -1
  102. package/dist/manager-caller.js +2 -1
  103. package/dist/manager-caller.js.map +1 -1
  104. package/dist/one-line.d.ts +6 -0
  105. package/dist/one-line.d.ts.map +1 -0
  106. package/dist/one-line.js +8 -0
  107. package/dist/one-line.js.map +1 -0
  108. package/dist/peer-label.d.ts +16 -0
  109. package/dist/peer-label.d.ts.map +1 -0
  110. package/dist/peer-label.js +15 -0
  111. package/dist/peer-label.js.map +1 -0
  112. package/dist/permission-denied.d.ts +8 -0
  113. package/dist/permission-denied.d.ts.map +1 -0
  114. package/dist/permission-denied.js +15 -0
  115. package/dist/permission-denied.js.map +1 -0
  116. package/dist/presence-text.d.ts +6 -0
  117. package/dist/presence-text.d.ts.map +1 -0
  118. package/dist/presence-text.js +25 -0
  119. package/dist/presence-text.js.map +1 -0
  120. package/dist/provision.d.ts.map +1 -1
  121. package/dist/provision.js +8 -2
  122. package/dist/provision.js.map +1 -1
  123. package/dist/resolve.d.ts +2 -13
  124. package/dist/resolve.d.ts.map +1 -1
  125. package/dist/resolve.js +2 -12
  126. package/dist/resolve.js.map +1 -1
  127. package/dist/run-driver-grants.d.ts +5 -0
  128. package/dist/run-driver-grants.d.ts.map +1 -1
  129. package/dist/run-driver-grants.js +16 -4
  130. package/dist/run-driver-grants.js.map +1 -1
  131. package/dist/run-migration.d.ts +21 -0
  132. package/dist/run-migration.d.ts.map +1 -1
  133. package/dist/run-migration.js +2 -27
  134. package/dist/run-migration.js.map +1 -1
  135. package/dist/run-notice.d.ts +20 -0
  136. package/dist/run-notice.d.ts.map +1 -1
  137. package/dist/run-notice.js +3 -28
  138. package/dist/run-notice.js.map +1 -1
  139. package/dist/schema-bounds.d.ts +24 -0
  140. package/dist/schema-bounds.d.ts.map +1 -0
  141. package/dist/schema-bounds.js +74 -0
  142. package/dist/schema-bounds.js.map +1 -0
  143. package/dist/schema-profile.d.ts +11 -24
  144. package/dist/schema-profile.d.ts.map +1 -1
  145. package/dist/schema-profile.js +16 -73
  146. package/dist/schema-profile.js.map +1 -1
  147. package/dist/subjects.d.ts.map +1 -1
  148. package/dist/subjects.js +1 -2
  149. package/dist/subjects.js.map +1 -1
  150. package/dist/turn-contract.d.ts.map +1 -1
  151. package/dist/turn-contract.js +14 -4
  152. package/dist/turn-contract.js.map +1 -1
  153. package/package.json +14 -1
@@ -1,124 +1,28 @@
1
- import { type NatsConnection } from "@nats-io/transport-node";
2
- import { type CompiledContract } from "./schema-profile.js";
3
- import { type GoalResultFact } from "./endpoint-action.js";
4
- import { type EpCaller } from "./endpoint-subjects.js";
5
- import type { EpVerbTarget, EpAttributedReply, EpScatterResult, EpInstanceLiveness } from "./endpoint-verbs.js";
6
- /** A resolved command contract: the compiled input/output validators (recompiled from the store,
7
- * digest-verified against the registered declaration) plus the command's §13.2 admission facts. */
8
- export interface ResolvedCommand {
9
- command: string;
10
- contract: {
11
- input: CompiledContract;
12
- output: CompiledContract;
13
- };
14
- class: string;
15
- targeted: boolean;
16
- modes: readonly string[];
17
- capability: string;
18
- }
19
- /** An endpoint's resolved invocation surface: every command the caller may see, with recompiled
20
- * digest-verified contracts. Built from a fresh `describe` + store fetch. Carries the `caller`
21
- * triple the describe ran as, so {@link invokeCommand} reuses the same authenticated identity,
22
- * and the ANSWERING incarnation's identity off the describe reply SUBJECT (broker-authenticated:
23
- * the §13.9 serve publish row pins `instanceId`+`epoch`, a responder cannot stamp another's) -
24
- * {@link invokeCommand}'s default currency check binds the invoke to this incarnation. */
25
- export interface ResolvedService {
26
- endpoint: string;
27
- owner: string;
28
- caller: EpCaller;
29
- responder: {
30
- instanceId: string;
31
- epoch: number;
32
- };
33
- commands: Map<string, ResolvedCommand>;
34
- /** Set when the service was resolved PINNED to one instance's `inst` route (`--on`):
35
- * {@link invokeCommand} then routes commands to that exact instance, never the class `one` queue,
36
- * so a multi-manager space can be addressed per-instance. Absent ⇒ class anycast (the default). */
37
- pinnedInstanceId?: string;
38
- }
39
- /**
40
- * The reserved `describe` command as a raw request/reply (§13.7: describe pins NO contract, so it
41
- * carries no `op` digests — {@link epCall} always stamps digests and the serve boundary rejects a
42
- * digest-bearing describe as `contract-mismatch`, so this is a purpose-built raw path). It
43
- * REQUEST-BINDS its reply exactly as {@link epCall}'s `parseAttributedReply` does (§13.2): the
44
- * responder grant `epResponderReplyPattern` spans EVERY caller suffix, so any live responder can
45
- * publish on the caller's rail at any nonce — acceptance therefore checks the reply SUBJECT's
46
- * endpoint + nonce AND the body's echoed request id, not just "first `{ok:true}` on the rail".
47
- * A reply that fails any of these is IGNORED (not rejected: an attacker racing a wrong-nonce reply
48
- * must not be able to fail an honest describe), and the wait continues to the deadline. A signal
49
- * that aborts with a `TimeoutError` ends the wait as that deadline would.
50
- */
51
- export declare function describeEndpoint(nc: NatsConnection, space: string, endpoint: string, caller: EpCaller, opts?: {
52
- deadlineMs?: number;
53
- instanceId?: string;
54
- signal?: AbortSignal;
55
- }): Promise<{
56
- answer: DescribeAnswer;
57
- responder: {
58
- instanceId: string;
59
- epoch: number;
60
- };
61
- }>;
62
- /** The describe answer shape a caller reads (a subset — the fields the resolver needs). */
63
- interface DescribeAnswer {
64
- public: boolean;
65
- descriptor: {
66
- endpoint: string;
67
- owner: string;
68
- clusters: {
69
- digest: string;
70
- commands: string[];
71
- }[];
72
- };
73
- }
74
- /**
75
- * DESCRIBE an endpoint and resolve its full invocation surface: send the reserved `describe`
76
- * command (untargeted, void args), then for every VISIBLE cluster fetch + verify its document
77
- * from the store and recompile each command's input/output contracts. The result lets a caller
78
- * invoke any visible command by name with no compile-time knowledge of the endpoint's schemas.
79
- *
80
- * `describe` itself pins no contract (§13.7), so it is issued as a raw void-arg request
81
- * ({@link describeEndpoint}), never through the digest-stamping {@link epCall}.
82
- */
83
- export declare function resolveService(nc: NatsConnection, space: string, endpoint: string, caller: EpCaller, opts?: {
84
- deadlineMs?: number;
85
- instanceId?: string;
86
- signal?: AbortSignal;
87
- }): Promise<ResolvedService>;
88
- /** How many times a call is re-issued after a `not-executed` bind refusal. Every
89
- * re-issue is a first attempt, so this is a loop guard and not a duplication guard. Each attempt
90
- * splits with probability (m-1)/m in a space of m managers, so seventeen attempts leave a
91
- * three-manager space about 1 in 1000 where the unrepaired call failed 2 in 3 (#398). An attempt
92
- * costs an answered describe and invoke, never an elapsed deadline. */
93
- export declare const BIND_SPLIT_REISSUES = 16;
94
1
  /**
95
- * INVOKE one named command on a resolved service: validate nothing here (the compiled input
96
- * contract in {@link epCall}'s request builder gates args before publish, and the responder's
97
- * digest-bound boundary re-validates), route on the `one` rail, return the attributed reply. A
98
- * command absent from the resolved surface is `not-found` (the caller cannot see it, or it does
99
- * not exist); a targeted command needs its `target`.
2
+ * The GENERIC caller path: describe an endpoint, fetch its
3
+ * registered contracts from the §13.7 content store, recompile the digest-matching validators,
4
+ * and invoke a named command — WITHOUT the caller compiling the endpoint's schemas ahead of time.
5
+ * This is what a `cotal describe`/`cotal invoke` CLI and every migrated control consumer ride, so
6
+ * a consumer no longer hand-imports the manager's contract module.
100
7
  *
101
- * Currency: `opts.currentEpoch` (e.g. the registry-read `serviceEpochReader`) when supplied;
102
- * otherwise the DESCRIBE-BOUND default - accept exactly the incarnation that answered this
103
- * service's resolve (its broker-authenticated `instanceId`+`epoch` off the describe reply
104
- * subject) and refuse `failed-precondition` when a DIFFERENT instance wins the `one` queue
105
- * (a superseded-or-split responder; re-resolve to adopt a legitimate successor). The bind needs
106
- * no registry read grant, and it is strictly stronger than no check: two live instances of a
107
- * single-instance endpoint can never both pass one resolved handle.
8
+ * The trust chain is the §13.7 one, end to end:
9
+ * - `describe` (the reserved, authorization-scoped command every endpoint serves) answers the
10
+ * caller's VISIBLE command set + the registered CLUSTER closure digests;
11
+ * - each cluster document is fetched from the store at its closure digest and VERIFIED
12
+ * (two-stage manifest→root, content-addressed) — the command's input/output CLOSURE digests
13
+ * come from those verified bytes, never a caller assertion;
14
+ * - each schema closure is fetched + PROFILE-recompiled; the recompiled contract's closureDigest
15
+ * MUST equal the registered digest (a store that served the wrong bytes fails here);
16
+ * - the invoke pins those digests, so the responder's digest-bound serve boundary honors exactly
17
+ * the schema the caller validated against.
108
18
  */
109
- export declare function invokeCommand(nc: NatsConnection, space: string, service: ResolvedService, command: string, args: Record<string, unknown> | undefined, opts: {
110
- target?: EpVerbTarget;
111
- deadlineMs?: number;
112
- signal?: AbortSignal;
113
- currentEpoch?: (instanceId: string) => Promise<number> | number;
114
- /** A caller-pinned envelope id (see {@link EpVerbOp.id}): a goal-accepting command binds its
115
- * goal under it, which is what lets a durable caller resubmit idempotently. */
116
- id?: string;
117
- }): Promise<EpAttributedReply>;
118
- /** The refusal a goal-following call meets on a handle whose endpoint serves no `goal-result`: an
119
- * accepted goal could never be read back to its terminal (SPEC 13.6), so nothing is submitted.
120
- * `undefined` when the handle can follow. */
121
- export declare function goalFollowRefusal(service: ResolvedService): EpAttributedReply | undefined;
19
+ import { type NatsConnection } from "@nats-io/transport-node";
20
+ import { type GoalResultFact } from "./endpoint-action.js";
21
+ import { type EpAttributedReply, type EpScatterResult, type EpInstanceLiveness } from "./endpoint-verbs.js";
22
+ import type { EpCaller } from "./endpoint-subjects.js";
23
+ import { type ResolvedService, invokeCommand } from "./endpoint-caller.js";
24
+ export { BIND_SPLIT_REISSUES, describeEndpoint, goalFollowRefusal, invokeCommand } from "./endpoint-caller.js";
25
+ export type { ResolvedCommand, ResolvedService } from "./endpoint-caller.js";
122
26
  /**
123
27
  * {@link invokeCommand} with a SPEC 13.2 bind refusal repaired rather than surfaced.
124
28
  *
@@ -141,6 +45,20 @@ export declare function goalFollowRefusal(service: ResolvedService): EpAttribute
141
45
  * answering about itself, and re-resolving on the class rail would reach another one.
142
46
  */
143
47
  export declare function invokeRepairingSplit(nc: NatsConnection, space: string, service: ResolvedService, command: string, args: Record<string, unknown> | undefined, opts: Parameters<typeof invokeCommand>[5], reresolve?: () => Promise<ResolvedService>): Promise<EpAttributedReply>;
48
+ /**
49
+ * DESCRIBE an endpoint and resolve its full invocation surface: send the reserved `describe`
50
+ * command (untargeted, void args), then for every VISIBLE cluster fetch + verify its document
51
+ * from the store and recompile each command's input/output contracts. The result lets a caller
52
+ * invoke any visible command by name with no compile-time knowledge of the endpoint's schemas.
53
+ *
54
+ * `describe` itself pins no contract (§13.7), so it is issued as a raw void-arg request
55
+ * ({@link describeEndpoint}), never through the digest-stamping {@link epCall}.
56
+ */
57
+ export declare function resolveService(nc: NatsConnection, space: string, endpoint: string, caller: EpCaller, opts?: {
58
+ deadlineMs?: number;
59
+ instanceId?: string;
60
+ signal?: AbortSignal;
61
+ }): Promise<ResolvedService>;
144
62
  export interface SubmitAndFollowGoalOptions {
145
63
  /** Read through the current authorized transport. Cancellation owns this read, not the goal. */
146
64
  reconcile?: (goalId: string, attributed: EpAttributedReply, context: {
@@ -193,5 +111,4 @@ export declare function scatterCommand(nc: NatsConnection, space: string, servic
193
111
  }): Promise<EpScatterResult>;
194
112
  /** A digest reference's bare hex, exported so a CLI can print the resolved surface's digests. */
195
113
  export declare function contractDigestHexOf(value: unknown): string;
196
- export {};
197
114
  //# sourceMappingURL=endpoint-invoke.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"endpoint-invoke.d.ts","sourceRoot":"","sources":["../src/endpoint-invoke.ts"],"names":[],"mappings":"AAmBA,OAAO,EAA4B,KAAK,cAAc,EAAqB,MAAM,yBAAyB,CAAC;AAI3G,OAAO,EAAmB,KAAK,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC7E,OAAO,EAEL,KAAK,cAAc,EAEpB,MAAM,sBAAsB,CAAC;AAQ9B,OAAO,EAAwE,KAAK,QAAQ,EAAgB,MAAM,wBAAwB,CAAC;AAE3I,OAAO,KAAK,EAAE,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AA0BhH;oGACoG;AACpG,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE;QAAE,KAAK,EAAE,gBAAgB,CAAC;QAAC,MAAM,EAAE,gBAAgB,CAAA;KAAE,CAAC;IAChE,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;2FAK2F;AAC3F,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,QAAQ,CAAC;IACjB,SAAS,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IACjD,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACvC;;wGAEoG;IACpG,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,cAAc,EAClB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,QAAQ,EAChB,IAAI,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAO,GAC5E,OAAO,CAAC;IAAE,MAAM,EAAE,cAAc,CAAC;IAAC,SAAS,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC,CA0FvF;AAmFD,2FAA2F;AAC3F,UAAU,cAAc;IACtB,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;SAAE,EAAE,CAAA;KAAE,CAAC;CACrG;AAED;;;;;;;;GAQG;AACH,wBAAsB,cAAc,CAClC,EAAE,EAAE,cAAc,EAClB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,QAAQ,EAChB,IAAI,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAO,GAC5E,OAAO,CAAC,eAAe,CAAC,CAmD1B;AAED;;;;wEAIwE;AACxE,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAEtC;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,aAAa,CACjC,EAAE,EAAE,cAAc,EAClB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EACzC,IAAI,EAAE;IACJ,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,YAAY,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;IAChE;oFACgF;IAChF,EAAE,CAAC,EAAE,MAAM,CAAC;CACb,GACA,OAAO,CAAC,iBAAiB,CAAC,CA6E5B;AAED;;8CAE8C;AAC9C,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,eAAe,GAAG,iBAAiB,GAAG,SAAS,CASzF;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,cAAc,EAClB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EACzC,IAAI,EAAE,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC,CAAC,CAAC,EACzC,SAAS,GAAE,MAAM,OAAO,CAAC,eAAe,CACiH,GACxJ,OAAO,CAAC,iBAAiB,CAAC,CAiB5B;AAED,MAAM,WAAW,0BAA0B;IACzC,gGAAgG;IAChG,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,iBAAiB,EAAE,OAAO,EAAE;QACnE,MAAM,EAAE,QAAQ,CAAC;QAAC,MAAM,EAAE,WAAW,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;KAC3D,KAAK,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,cAAc,CAAA;KAAE,GAAG,SAAS,CAAC,CAAC;IACvE,SAAS,CAAC,EAAE,MAAM,cAAc,GAAG,SAAS,CAAC;IAC7C,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;IACvE,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;0BAIsB;IACtB,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACpE;AAED;;;;kFAIkF;AAClF,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,cAAc,EAClB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,QAAQ,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,CAAC,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,iBAAiB,CAAC,EAC5D,IAAI,CAAC,EAAE,0BAA0B,GAChC,OAAO,CAAC,iBAAiB,CAAC,CA8N5B;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,cAAc,CAClC,EAAE,EAAE,cAAc,EAClB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EACzC,IAAI,EAAE;IACJ,UAAU,EAAE,MAAM,CAAC;IAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IACvE;;wDAEoD;IACpD,aAAa,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAC;CACrE,GACA,OAAO,CAAC,eAAe,CAAC,CAuB1B;AAED,iGAAiG;AACjG,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAE1D"}
1
+ {"version":3,"file":"endpoint-invoke.d.ts","sourceRoot":"","sources":["../src/endpoint-invoke.ts"],"names":[],"mappings":"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"}
@@ -16,156 +16,59 @@
16
16
  * - the invoke pins those digests, so the responder's digest-bound serve boundary honors exactly
17
17
  * the schema the caller validated against.
18
18
  */
19
- import { randomBytes, randomUUID } from "node:crypto";
20
19
  import { PermissionViolationError } from "@nats-io/transport-node";
21
- import { openPublishDenialWatch } from "./endpoint-publish-denial.js";
22
20
  import { jetstreamManager } from "@nats-io/jetstream";
23
- import { EpEnvelopeError, EP_UNBOUND_RESPONDER, EP_UNANSWERED, renderLifecycleBlocked, lifecycleBlockedFrom, replyRefusedBeforeEffect, replyTargetUnmapped, unansweredRequest } from "./endpoint-envelope.js";
21
+ import { EpEnvelopeError, renderLifecycleBlocked, lifecycleBlockedFrom, replyRefusedBeforeEffect, replyTargetUnmapped, unansweredRequest } from "./endpoint-envelope.js";
24
22
  import { compileContract } from "./schema-profile.js";
25
- import { parseGoalResultFact, } from "./endpoint-action.js";
26
- import { contractStoreContext, fetchContractClosure, contractRefToHex, contractArtifactDigestHex, } from "./endpoint-contract-store.js";
23
+ import { parseGoalResultFact } from "./endpoint-action.js";
24
+ import { contractStoreContext, fetchContractClosure, contractRefToHex, contractArtifactDigestHex } from "./endpoint-contract-store.js";
27
25
  import { parseClusterDocument } from "./endpoint-cluster.js";
28
- import { epCall, epScatterService } from "./endpoint-verbs.js";
26
+ import { epScatterService } from "./endpoint-verbs.js";
29
27
  import { epGoalProgressGrantRow } from "./endpoint-grants.js";
30
- import { epRequestSubject, epCallerReplyFilter, epPlaneTokens, parseEpSubject } from "./endpoint-subjects.js";
31
- import { parseEndpointReply } from "./endpoint-envelope.js";
32
- const dec = new TextDecoder(), enc = new TextEncoder();
33
- const nonce = () => randomBytes(24).toString("base64url");
34
- /** A describe is the reserved read-only discovery bootstrap, so an unanswered request may be
35
- * re-published within its ORIGINAL deadline. Core NATS does not retain a request sent before a
36
- * responder subscribes; without this bounded retry, a responder that registers one moment later
37
- * is invisible until the caller pays the whole deadline. Commands are never retried here. */
38
- const DESCRIBE_RETRY_MS = 250;
39
- /** Format a caught value on a path that cannot afford to throw (timer/callback). A
40
- * poisoned `Error.message` getter or `toString` must not escape the catch; the
41
- * fallback is the detail, never a reason to skip the rejection. */
42
- function caughtText(e, fallback) {
43
- try {
44
- return e instanceof Error ? e.message : String(e);
45
- }
46
- catch {
47
- return fallback;
48
- }
49
- }
50
- /** Delivery margin above an action owner's accepted readiness budget. Spawn's established generic
51
- * invariant was 40s client > 30s manager; preserve that measured 10s separation when a connector
52
- * declares a different budget instead of inventing a second timeout policy. */
53
- const GOAL_FOLLOW_MARGIN_MS = 10_000;
28
+ import { BIND_SPLIT_REISSUES, GOAL_FOLLOW_MARGIN_MS, dec, describeEndpoint, invokeCommand } from "./endpoint-caller.js";
29
+ export { BIND_SPLIT_REISSUES, describeEndpoint, goalFollowRefusal, invokeCommand } from "./endpoint-caller.js";
54
30
  /**
55
- * The reserved `describe` command as a raw request/reply (§13.7: describe pins NO contract, so it
56
- * carries no `op` digests — {@link epCall} always stamps digests and the serve boundary rejects a
57
- * digest-bearing describe as `contract-mismatch`, so this is a purpose-built raw path). It
58
- * REQUEST-BINDS its reply exactly as {@link epCall}'s `parseAttributedReply` does (§13.2): the
59
- * responder grant `epResponderReplyPattern` spans EVERY caller suffix, so any live responder can
60
- * publish on the caller's rail at any nonce — acceptance therefore checks the reply SUBJECT's
61
- * endpoint + nonce AND the body's echoed request id, not just "first `{ok:true}` on the rail".
62
- * A reply that fails any of these is IGNORED (not rejected: an attacker racing a wrong-nonce reply
63
- * must not be able to fail an honest describe), and the wait continues to the deadline. A signal
64
- * that aborts with a `TimeoutError` ends the wait as that deadline would.
31
+ * {@link invokeCommand} with a SPEC 13.2 bind refusal repaired rather than surfaced.
32
+ *
33
+ * An unpinned handle binds the instance that answered its describe, and the invoke is a second,
34
+ * independent trip through the same class queue, so in a multi-instance space another member
35
+ * routinely receives it and refuses before dispatching. That refusal states the command did not
36
+ * run ({@link replyRefusedBeforeEffect}), so re-resolving and re-issuing is a first attempt and is
37
+ * safe for any command, mutations included. A targeted call that reaches a member holding no
38
+ * mapping for its target ({@link replyTargetUnmapped}) is re-issued the same way, because with an
39
+ * instance-local resolver only the member hosting the target can serve it. The re-issue repeats
40
+ * until a member serves the call or {@link BIND_SPLIT_REISSUES} runs out, and then the last refusal
41
+ * surfaces unchanged, or the last no-mapping refusal when there was one.
42
+ *
43
+ * `reresolve` defaults to a fresh class resolve as the handle's caller; a caller that memoizes its
44
+ * handle passes one that drops the memo first, so later calls do not start from the refused bind.
45
+ * When it throws, the refusal surfaces, because it states that nothing ran, which the resolve
46
+ * failure raised in its place would lose.
47
+ *
48
+ * A pinned handle is never repaired: it names its instance, so a refusal from it is that instance
49
+ * answering about itself, and re-resolving on the class rail would reach another one.
65
50
  */
66
- export async function describeEndpoint(nc, space, endpoint, caller, opts = {}) {
67
- opts.signal?.throwIfAborted();
68
- const deadlineMs = opts.deadlineMs ?? 10_000;
69
- const n = nonce();
70
- const requestId = nonce();
71
- // `--on <instance>`: PIN the describe to one instance's `inst` route so a multi-manager
72
- // space resolves the exact instance addressed, not whichever wins the class `one` queue. Default =
73
- // class anycast (mode "one"), unchanged for every existing caller.
74
- const route = opts.instanceId !== undefined ? { mode: "inst", instanceId: opts.instanceId } : { mode: "one" };
75
- const subject = epRequestSubject(space, { route, endpoint, command: "describe", caller, nonce: n });
76
- // The plane this describe rides, recorded on the unanswered marker so a surface that renders a
77
- // reachability verdict can scope it (SPEC 13.15: the legacy and versioned rails are disjoint subject spaces,
78
- // an endpoint serves both, and a caller holds rows on one of them only). Silence on the versioned rail is
79
- // therefore consistent with a responder that predates the versioned rail and serves `ep` alone.
80
- const rail = epPlaneTokens(caller).join(".");
81
- const env = {
82
- v: 1, id: requestId, op: { endpoint, command: "describe" }, class: "ephemeral",
83
- replyExpected: true, deadlineMs, from: { id: `${caller.owner}.${caller.actor}`, name: caller.actor },
84
- };
85
- let sub;
86
- let timer;
87
- let retryTimer;
88
- let denialWatch;
89
- let onAbort;
90
- const startedAt = performance.now();
91
- const unanswered = (ms) => new EpEnvelopeError("deadline-exceeded", `no describe reply from ${endpoint}${opts.instanceId !== undefined ? ` instance ${opts.instanceId}` : ""} within ${ms}ms on the ${rail} rail`, [{ kind: EP_UNANSWERED, endpoint, command: "describe", rail }]);
92
- const cancelled = new Promise((_, reject) => {
93
- onAbort = () => reject(opts.signal?.reason?.name === "TimeoutError"
94
- ? unanswered(Math.round(performance.now() - startedAt))
95
- : new EpEnvelopeError("unavailable", `describe(${endpoint}) observation cancelled`));
96
- opts.signal?.addEventListener("abort", onAbort, { once: true });
97
- });
98
- try {
99
- // REGISTER THE PERMISSION WATCH BEFORE THE PUBLISH IT IS WATCHING. See
100
- // {@link openPublishDenialWatch}: a refused publish is otherwise indistinguishable
101
- // from an unanswered describe.
102
- denialWatch = openPublishDenialWatch(nc, subject, () => new EpEnvelopeError("permission-denied", `the describe for ${endpoint} was REFUSED BY THE BROKER, not unanswered: this caller's credential does not authorize publishing to "${subject}"${opts.instanceId !== undefined ? ` (the instance rail for ${opts.instanceId}: an instance-addressed call needs a credential minted with that instance, not a class-rail one)` : ""}. The responder may be perfectly healthy; the grant is what is missing (SPEC 13.2)`, undefined, "not-executed"), "describe");
103
- const got = new Promise((resolve, reject) => {
104
- sub = nc.subscribe(epCallerReplyFilter(space, caller), {
105
- callback: (err, msg) => {
106
- if (err) {
107
- reject(new EpEnvelopeError("unavailable", `describe reply subscription failed: ${err.message}`));
108
- return;
109
- }
110
- // REQUEST-BIND off the reply SUBJECT first (§13.2): the responder triple + nonce are
111
- // broker-pinned by the serve publish grant. A reply for a DIFFERENT endpoint, or on a
112
- // nonce that is not the one we published (the rail is shared across our concurrent
113
- // requests, and a hostile responder can publish at any nonce), is NOT ours — ignore it
114
- // and keep waiting, never fail the honest describe on an injected reply.
115
- const parsed = parseEpSubject(msg.subject);
116
- if (!parsed || parsed.plane !== "reply" || parsed.endpoint !== endpoint || parsed.nonce !== n)
117
- return;
118
- // Then the body: it must parse as an EndpointReply and ECHO our request id on this
119
- // nonce-scoped rail (§13.3) — the second half of the confused-deputy binding.
120
- let reply;
121
- try {
122
- reply = parseEndpointReply(JSON.parse(dec.decode(msg.data)));
123
- }
124
- catch {
125
- return;
126
- } // a malformed body on our nonce is not a usable answer; wait for a valid one
127
- if (reply.id !== requestId)
128
- return;
129
- resolve({ body: reply, responder: { instanceId: parsed.instanceId, epoch: parsed.epoch } });
130
- },
131
- });
132
- const request = enc.encode(JSON.stringify(env));
133
- nc.publish(subject, request);
134
- // The first publish may precede the responder's subscription during startup. Re-publish the
135
- // SAME read-only describe under the SAME request binding until one answer wins or the original
136
- // deadline expires; this neither extends the budget nor retries the command being resolved.
137
- // `publish` throws synchronously after close/drain. A timer throw escapes the caller's promise
138
- // and crashes the process, so make transport loss settle this describe instead.
139
- retryTimer = setInterval(() => {
140
- try {
141
- nc.publish(subject, request);
142
- }
143
- catch (e) {
144
- reject(new EpEnvelopeError("unavailable", `the describe retry for ${endpoint} could not publish: ${caughtText(e, "unknown publish failure")}`));
145
- }
146
- }, DESCRIBE_RETRY_MS);
147
- });
148
- const timeout = new Promise((_, reject) => { timer = setTimeout(() => reject(unanswered(deadlineMs)), deadlineMs); });
149
- const { body: reply, responder } = await Promise.race([got, timeout, denialWatch.denied, cancelled]);
150
- if (reply.ok !== true) {
151
- // A responder ANSWERED with a refusal: it is rethrown under the responder's own code (which
152
- // may be `unavailable`) and deliberately without the EP_UNANSWERED marker the deadline above
153
- // carries, so a consumer keyed on that marker never reads an answering responder as absent.
154
- const e = reply.error;
155
- throw new EpEnvelopeError(e?.code ?? "unavailable", `describe(${endpoint}) failed: ${e?.message ?? "unknown"}`);
51
+ export async function invokeRepairingSplit(nc, space, service, command, args, opts, reresolve = () => resolveService(nc, space, service.endpoint, service.caller, { deadlineMs: opts.deadlineMs ?? 10_000, ...(opts.signal ? { signal: opts.signal } : {}) })) {
52
+ let handle = service;
53
+ // A member that looked the target up and found no mapping says more about it than a bind refusal,
54
+ // which says nothing, so a call that runs out of re-issues surfaces the latest such refusal.
55
+ let unmapped;
56
+ for (let reissues = 0;; reissues += 1) {
57
+ const r = await invokeCommand(nc, space, handle, command, args, opts);
58
+ if (replyTargetUnmapped(r.reply.error))
59
+ unmapped = r;
60
+ else if (r.reply.ok !== false || !replyRefusedBeforeEffect(r.reply.error))
61
+ return r;
62
+ if (handle.pinnedInstanceId !== undefined)
63
+ return r;
64
+ if (reissues === BIND_SPLIT_REISSUES)
65
+ return unmapped ?? r;
66
+ try {
67
+ handle = await reresolve();
68
+ }
69
+ catch {
70
+ return unmapped ?? r;
156
71
  }
157
- return { answer: reply.data, responder };
158
- }
159
- finally {
160
- sub?.unsubscribe();
161
- if (timer !== undefined)
162
- clearTimeout(timer);
163
- if (retryTimer !== undefined)
164
- clearInterval(retryTimer);
165
- if (onAbort)
166
- opts.signal?.removeEventListener("abort", onAbort);
167
- // Release on EVERY exit, success included. See {@link openPublishDenialWatch}.
168
- denialWatch?.release();
169
72
  }
170
73
  }
171
74
  /** Fetch + verify ONE cluster document from the store at its closure digest (two-stage §13.7:
@@ -234,7 +137,8 @@ function extractSchemaRefs(bytes) {
234
137
  * so is this one. High enough that real surfaces overlap freely, low enough to stay a bound. */
235
138
  const RESOLVE_MAX_INFLIGHT_READS = 32;
236
139
  /** Run `work` over `items` with at most `limit` in flight, preserving RESULT ORDER (the resolved
237
- * surface must not depend on read timing). Rejects like `Promise.all`: the first failure wins. */
140
+ * surface must not depend on read timing). Rejects like `Promise.all`: the first failure wins,
141
+ * and no item starts after it. */
238
142
  async function pooled(items, limit, work) {
239
143
  const out = new Array(items.length);
240
144
  let next = 0;
@@ -243,7 +147,15 @@ async function pooled(items, limit, work) {
243
147
  const i = next++;
244
148
  if (i >= items.length)
245
149
  return;
246
- out[i] = await work(items[i]);
150
+ try {
151
+ out[i] = await work(items[i]);
152
+ }
153
+ catch (e) {
154
+ // This failure rejects the pool, so a later item would spend store reads on a result
155
+ // nobody reads. Items already in flight finish on their own.
156
+ next = items.length;
157
+ throw e;
158
+ }
247
159
  }
248
160
  };
249
161
  await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
@@ -311,162 +223,6 @@ export async function resolveService(nc, space, endpoint, caller, opts = {}) {
311
223
  commands.set(rc.command, rc);
312
224
  return { endpoint: answer.descriptor.endpoint, owner: answer.descriptor.owner, caller, responder, commands, ...(opts.instanceId !== undefined ? { pinnedInstanceId: opts.instanceId } : {}) };
313
225
  }
314
- /** How many times a call is re-issued after a `not-executed` bind refusal. Every
315
- * re-issue is a first attempt, so this is a loop guard and not a duplication guard. Each attempt
316
- * splits with probability (m-1)/m in a space of m managers, so seventeen attempts leave a
317
- * three-manager space about 1 in 1000 where the unrepaired call failed 2 in 3 (#398). An attempt
318
- * costs an answered describe and invoke, never an elapsed deadline. */
319
- export const BIND_SPLIT_REISSUES = 16;
320
- /**
321
- * INVOKE one named command on a resolved service: validate nothing here (the compiled input
322
- * contract in {@link epCall}'s request builder gates args before publish, and the responder's
323
- * digest-bound boundary re-validates), route on the `one` rail, return the attributed reply. A
324
- * command absent from the resolved surface is `not-found` (the caller cannot see it, or it does
325
- * not exist); a targeted command needs its `target`.
326
- *
327
- * Currency: `opts.currentEpoch` (e.g. the registry-read `serviceEpochReader`) when supplied;
328
- * otherwise the DESCRIBE-BOUND default - accept exactly the incarnation that answered this
329
- * service's resolve (its broker-authenticated `instanceId`+`epoch` off the describe reply
330
- * subject) and refuse `failed-precondition` when a DIFFERENT instance wins the `one` queue
331
- * (a superseded-or-split responder; re-resolve to adopt a legitimate successor). The bind needs
332
- * no registry read grant, and it is strictly stronger than no check: two live instances of a
333
- * single-instance endpoint can never both pass one resolved handle.
334
- */
335
- export async function invokeCommand(nc, space, service, command, args, opts) {
336
- const resolved = service.commands.get(command);
337
- if (resolved === undefined)
338
- throw new EpEnvelopeError("not-found", `command "${command}" is not in ${service.endpoint}'s visible surface; describe lists ${[...service.commands.keys()].sort().join(", ") || "(none)"}`);
339
- if (resolved.targeted && opts.target === undefined)
340
- throw new EpEnvelopeError("bad-request", `command "${command}" is targeted (modes: ${resolved.modes.join(", ")}); an invoke needs its target`);
341
- if (!resolved.targeted && opts.target !== undefined)
342
- throw new EpEnvelopeError("bad-request", `command "${command}" is untargeted; an invoke must not carry a target`);
343
- const caller = service.caller;
344
- // "No args" marshals to the CONTRACT's canonical empty form: absent args ride as null on the
345
- // wire, so when this command's input rejects null but accepts the empty object (e.g. an
346
- // all-optional `{type:"object"}` input like despawn's), send `{}` — that IS the caller's
347
- // intent in that contract's vocabulary (a targeted CLI stop has nothing left after the alias
348
- // becomes the target block). Contract-derived, never a guess: an input that requires fields
349
- // accepts neither form and still refuses loud at the pre-publish validation below.
350
- let sendArgs = args;
351
- if (sendArgs === undefined && !resolved.contract.input.validate(null) && resolved.contract.input.validate({}))
352
- sendArgs = {};
353
- const describeBound = (instanceId) => {
354
- if (instanceId !== service.responder.instanceId) {
355
- // The refusal is unchanged; what it SAYS is not. One message used to cover two situations
356
- // that call for opposite responses, and it described only the rarer one.
357
- //
358
- // PINNED: the caller named an instance and a different one answered. Genuinely wrong.
359
- //
360
- // UNPINNED: the caller addressed the CLASS. The describe and the invoke are two independent
361
- // trips through the same anycast queue, so in a multi-instance space they routinely land on
362
- // different instances and this fires on an ordinary, correct request - not on a supersession.
363
- // Calling that "superseded-or-split" sends the reader hunting a restart that never happened.
364
- // The old text's remedy is also not executable by the surfaces that print it most (`stop`,
365
- // `despawn` and `attach` have no adopt path and no pin), so it advised an action the caller
366
- // could not take. Say which case this is, and stop asserting a cause that is usually wrong.
367
- // The marker, not the prose, is what stops an automatic re-invoke: reaching here means a
368
- // responder ANSWERED (executed or refused; the reply does not say which), so a retry is a
369
- // second attempt that may duplicate an effect rather than a repair. Callers that recover
370
- // from `failed-precondition` by re-resolving must consult `respondedButUnbound`.
371
- // The remedy is stated in core vocabulary (address one instance), never as a CLI flag: core
372
- // cannot know whether its caller has one, and most `invokeService` callers (a connector's
373
- // tools, a manifest deploy) do not. The CLI names its own flag when it renders this.
374
- throw new EpEnvelopeError("failed-precondition", service.pinnedInstanceId !== undefined
375
- ? `the ${service.endpoint} instance ${instanceId} answered but this handle is PINNED to ${service.pinnedInstanceId}; a pinned call names its instance and never accepts another. ${instanceId} did receive and answer the request, so if "${command}" mutates, that effect may already have landed - verify before re-issuing (SPEC 13.2)`
376
- : `the ${service.endpoint} instance ${instanceId} won the class queue but this UNPINNED handle resolved against ${service.responder.instanceId}; the describe and the invoke are separate trips through the same queue, so in a multi-instance space this is an ordinary split and not necessarily a supersession - the handle cannot currently adopt a different winner. THIS SAYS NOTHING ABOUT WHETHER THE COMMAND RAN: ${instanceId} received the request and answered it, possibly after this error was raised. For a read that is harmless and re-issuing is safe; if "${command}" mutates, verify the outcome ('ps'/'inspect'/roster) before re-issuing, because a retry that assumes failure duplicates the effect. A call that addresses one instance does not split (SPEC 13.2)`, [{ kind: EP_UNBOUND_RESPONDER, endpoint: service.endpoint, command, answeredBy: instanceId, boundTo: service.responder.instanceId, pinned: service.pinnedInstanceId !== undefined }]);
377
- }
378
- return service.responder.epoch;
379
- };
380
- // `--on`: a PINNED service routes to its exact instance's `inst` rail (the same instance the
381
- // describe resolved to, at its resolved epoch), never the class `one` queue — so the command reaches
382
- // the addressed manager in a multi-manager space. Unpinned ⇒ class anycast `one` (unchanged). The
383
- // describeBound currency check still holds: an inst-routed reply carries that instance's id.
384
- const route = service.pinnedInstanceId !== undefined
385
- ? { mode: "inst", instanceId: service.pinnedInstanceId, epoch: service.responder.epoch }
386
- : { mode: "one" };
387
- // The bind travels WITH the request, so the responder can refuse before running the command
388
- // rather than leaving `describeBound` to report the split afterwards. It is sent exactly when
389
- // this handle's own resolve is the currency reference — that is the caller saying "this
390
- // incarnation or none", and it is the same population the check below already refuses. A caller
391
- // that supplied its own `currentEpoch` is asking a REGISTRY whether the answerer is current, and
392
- // deliberately accepts any current member; binding it would refuse calls that succeed today.
393
- const bind = opts.currentEpoch === undefined
394
- ? { instanceId: service.pinnedInstanceId ?? service.responder.instanceId, epoch: service.responder.epoch }
395
- : undefined;
396
- return epCall(nc, space, route, {
397
- endpoint: service.endpoint, command, contract: resolved.contract, caller,
398
- ...(sendArgs !== undefined ? { args: sendArgs } : {}),
399
- ...(opts.target ? { target: opts.target } : {}),
400
- ...(bind !== undefined ? { bind } : {}),
401
- ...(opts.id !== undefined ? { id: opts.id } : {}),
402
- }, {
403
- deadlineMs: opts.deadlineMs ?? 10_000,
404
- signal: opts.signal,
405
- currentEpoch: opts.currentEpoch ?? describeBound,
406
- // What the currency hook returns decides how a stale-epoch refusal is worded and marked: the
407
- // describe-bound default is this handle's own bind (a responder ahead of it is a successor and
408
- // the handle is the stale side); a caller-supplied hook is a registry read by epCall's contract.
409
- currencyReference: opts.currentEpoch ? "registry" : "bind",
410
- });
411
- }
412
- /** The refusal a goal-following call meets on a handle whose endpoint serves no `goal-result`: an
413
- * accepted goal could never be read back to its terminal (SPEC 13.6), so nothing is submitted.
414
- * `undefined` when the handle can follow. */
415
- export function goalFollowRefusal(service) {
416
- if (service.commands.has("goal-result"))
417
- return undefined;
418
- return {
419
- reply: { v: 1, id: randomUUID(), ok: false, error: {
420
- code: "failed-precondition", outcome: "not-executed",
421
- message: `endpoint "${service.endpoint}" does not support "goal-result"; upgrade manager to enable durable goal following (SPEC 13.6)`,
422
- } },
423
- responder: { endpoint: service.endpoint, instanceId: service.responder.instanceId, epoch: service.responder.epoch },
424
- };
425
- }
426
- /**
427
- * {@link invokeCommand} with a SPEC 13.2 bind refusal repaired rather than surfaced.
428
- *
429
- * An unpinned handle binds the instance that answered its describe, and the invoke is a second,
430
- * independent trip through the same class queue, so in a multi-instance space another member
431
- * routinely receives it and refuses before dispatching. That refusal states the command did not
432
- * run ({@link replyRefusedBeforeEffect}), so re-resolving and re-issuing is a first attempt and is
433
- * safe for any command, mutations included. A targeted call that reaches a member holding no
434
- * mapping for its target ({@link replyTargetUnmapped}) is re-issued the same way, because with an
435
- * instance-local resolver only the member hosting the target can serve it. The re-issue repeats
436
- * until a member serves the call or {@link BIND_SPLIT_REISSUES} runs out, and then the last refusal
437
- * surfaces unchanged, or the last no-mapping refusal when there was one.
438
- *
439
- * `reresolve` defaults to a fresh class resolve as the handle's caller; a caller that memoizes its
440
- * handle passes one that drops the memo first, so later calls do not start from the refused bind.
441
- * When it throws, the refusal surfaces, because it states that nothing ran, which the resolve
442
- * failure raised in its place would lose.
443
- *
444
- * A pinned handle is never repaired: it names its instance, so a refusal from it is that instance
445
- * answering about itself, and re-resolving on the class rail would reach another one.
446
- */
447
- export async function invokeRepairingSplit(nc, space, service, command, args, opts, reresolve = () => resolveService(nc, space, service.endpoint, service.caller, { deadlineMs: opts.deadlineMs ?? 10_000, ...(opts.signal ? { signal: opts.signal } : {}) })) {
448
- let handle = service;
449
- // A member that looked the target up and found no mapping says more about it than a bind refusal,
450
- // which says nothing, so a call that runs out of re-issues surfaces the latest such refusal.
451
- let unmapped;
452
- for (let reissues = 0;; reissues += 1) {
453
- const r = await invokeCommand(nc, space, handle, command, args, opts);
454
- if (replyTargetUnmapped(r.reply.error))
455
- unmapped = r;
456
- else if (r.reply.ok !== false || !replyRefusedBeforeEffect(r.reply.error))
457
- return r;
458
- if (handle.pinnedInstanceId !== undefined)
459
- return r;
460
- if (reissues === BIND_SPLIT_REISSUES)
461
- return unmapped ?? r;
462
- try {
463
- handle = await reresolve();
464
- }
465
- catch {
466
- return unmapped ?? r;
467
- }
468
- }
469
- }
470
226
  /** Subscribe before a single submission, then observe its accepted goal through live progress and
471
227
  * mediated canonical reads. Local failures before an attributed reply throw; they never invent a
472
228
  * responder. Once accepted, observation failures retain that responder and never authorize retry.