esoul-sdk 0.21.0 → 0.21.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  Releases before 0.20.0 are recorded in the repository history only.
4
4
 
5
+ ## 0.21.1
6
+
7
+ ### Fixed
8
+
9
+ - **SimGmail people answer an agent's mail.** A persona that is a person (`reply`, `optout`) now reads and
10
+ answers mail marked `Auto-Submitted` (what an app's autonomous agent sends, RFC 3834) — as people do. Only a
11
+ machine responder (`ooo`) stays silent to it, and a dead address bounces it. Before, every persona ignored
12
+ it, so a simulated conversation with an autonomous agent stopped after the first reply.
13
+
14
+ ### Added
15
+
16
+ - **`esoul-sdk/testing` exports the failed-request list** (`listFailedRequests`, `subscribeFailedRequests`,
17
+ `resetFailedRequests`) so a test can assert that a click which failed reverted and was named on the banner
18
+ (`runOptimistic` from `esoul-sdk/react`).
19
+
5
20
  ## 0.21.0
6
21
 
7
22
  ### Added
package/api-reference.md CHANGED
@@ -3928,11 +3928,11 @@ interface WorkspaceTools {
3928
3928
  ```
3929
3929
 
3930
3930
  ==============================================================================
3931
- ## `esoul-sdk/testing` — 53 exports
3931
+ ## `esoul-sdk/testing` — 56 exports
3932
3932
 
3933
3933
  Tests: run an op as a persona, an in-memory database with the real rules, recorders.
3934
3934
 
3935
- ### Functions and values (13)
3935
+ ### Functions and values (16)
3936
3936
 
3937
3937
  #### `capture` — function · src/testing/ops.ts
3938
3938
 
@@ -3982,6 +3982,12 @@ A caller to run something as. Give it a `role` when your app declares its own vo
3982
3982
  function fakeViewer( kind: ViewerKind, opts: { userId?: string | null; viewerIds?: string[]; role?: string; name?: string | null; email?: string | null; attrs?: Record<string, string | number | boolean> } = {}, ): RuleViewer
3983
3983
  ```
3984
3984
 
3985
+ #### `listFailedRequests` — function · src/failed-requests.ts
3986
+
3987
+ ```ts
3988
+ function listFailedRequests(): FailedRequest[]
3989
+ ```
3990
+
3985
3991
  #### `memoryDb` — function · src/testing/db.ts
3986
3992
 
3987
3993
  A database for your app, from your manifest. Starts as the app's own code (an `internal` caller) so a test can seed rows, then `.as(someone)` to check what each kind of person may actually see.
@@ -3998,6 +4004,14 @@ An in-memory `FilesApi` over a tree you write in the test: folders are objects,
3998
4004
  function memoryFiles(sources: Record<string, MemoryFileTree>, opts?: { readOnly?: string[] }): MemoryFiles
3999
4005
  ```
4000
4006
 
4007
+ #### `resetFailedRequests` — function · src/failed-requests.ts
4008
+
4009
+ Test-only: start from nothing.
4010
+
4011
+ ```ts
4012
+ function __resetFailedRequests(): void
4013
+ ```
4014
+
4001
4015
  #### `roleForKind` — function · src/testing/db.ts
4002
4016
 
4003
4017
  The app's own word for a kind of caller, read from the manifest's `roles`.
@@ -4038,6 +4052,12 @@ A real (tiny) OAuth2 provider for exercising the plugin-connection flow: /author
4038
4052
  function startMockOAuth(opts?: { port?: number; clientId?: string; accessTtlMs?: number; }): Promise<MockOAuthServer>
4039
4053
  ```
4040
4054
 
4055
+ #### `subscribeFailedRequests` — function · src/failed-requests.ts
4056
+
4057
+ ```ts
4058
+ function subscribeFailedRequests(l: Listener): () => void
4059
+ ```
4060
+
4041
4061
  ### Types (40)
4042
4062
 
4043
4063
  #### `BoundAppFixture` — interface · src/testing/ops.ts
@@ -18,6 +18,8 @@ export { capture, fakeApps, runOp } from "./ops.js";
18
18
  export type { BoundAppFixture, EmitCall, NotifyCall, Recorder, RunOpOptions, RunOpResult } from "./ops.js";
19
19
  export { memoryFiles } from "./files.js";
20
20
  export { scriptedModel, defaultScriptedText } from "./llm.js";
21
+ /** The failed-request banner's list, for a test that asserts a click reverted and was named (runOptimistic). */
22
+ export { listFailedRequests, subscribeFailedRequests, __resetFailedRequests as resetFailedRequests } from "../failed-requests.js";
21
23
  export type { ScriptedLlm, ScriptedPolicy, ScriptedReply } from "./llm.js";
22
24
  export type { MemoryFiles, MemoryFileTree } from "./files.js";
23
25
  export { simGmail, DEFAULT_SIM_ACCOUNT, DEFAULT_CROWD_MIX } from "./sim-gmail/index.js";
@@ -16,6 +16,8 @@ export { fakeViewer, memoryDb, roleForKind } from "./db.js";
16
16
  export { capture, fakeApps, runOp } from "./ops.js";
17
17
  export { memoryFiles } from "./files.js";
18
18
  export { scriptedModel, defaultScriptedText } from "./llm.js";
19
+ /** The failed-request banner's list, for a test that asserts a click reverted and was named (runOptimistic). */
20
+ export { listFailedRequests, subscribeFailedRequests, __resetFailedRequests as resetFailedRequests } from "../failed-requests.js";
19
21
  export { simGmail, DEFAULT_SIM_ACCOUNT, DEFAULT_CROWD_MIX } from "./sim-gmail/index.js";
20
22
  import http from "node:http";
21
23
  import crypto from "node:crypto";
@@ -26,6 +26,8 @@ export declare class Personas {
26
26
  crowd(n: number, mix?: CrowdMix, opts?: CrowdOptions, record?: boolean): CrowdMember[];
27
27
  private inboundView;
28
28
  private answerable;
29
+ /** Mail a machine sent (`Auto-Submitted` other than "no"): only a machine responder ignores it. */
30
+ private automated;
29
31
  private select;
30
32
  private intercept;
31
33
  private delivered;
@@ -7,8 +7,11 @@
7
7
  * travel through the same send path as an API send, after `latencyMs` of
8
8
  * virtual time (0 = before the sender's next request).
9
9
  *
10
- * Personas never answer auto-submitted mail, mailer-daemon, or another
11
- * persona — so two scripts cannot loop.
10
+ * Personas never answer mailer-daemon or another persona, so two scripts cannot
11
+ * loop. Auto-submitted mail (RFC 3834 — an agent's autonomous reply carries
12
+ * `Auto-Submitted: auto-replied`) is answered the way the world answers it: a
13
+ * PERSON (reply / opt-out) reads and answers it like any mail, a machine
14
+ * responder (out-of-office) stays silent, and a dead address bounces it.
12
15
  */
13
16
  import { buildMime, formatAddress, headerOf, parseAddressList, textOf } from "./mime.js";
14
17
  import { rfc2822 } from "./corpus.js";
@@ -99,13 +102,15 @@ export class Personas {
99
102
  count,
100
103
  };
101
104
  }
102
- answerable(sender, msg, node = null) {
105
+ answerable(sender, msg) {
103
106
  if (sender.persona)
104
107
  return false;
105
- if (/mailer-daemon@|postmaster@/i.test(msg.from))
106
- return false;
108
+ return !/mailer-daemon@|postmaster@/i.test(msg.from);
109
+ }
110
+ /** Mail a machine sent (`Auto-Submitted` other than "no"): only a machine responder ignores it. */
111
+ automated(node) {
107
112
  const auto = node ? headerOf(node, "auto-submitted") : undefined;
108
- return !auto || auto.toLowerCase() === "no";
113
+ return !!auto && auto.toLowerCase() !== "no";
109
114
  }
110
115
  select(state, inbound) {
111
116
  for (let i = 0; i < state.steps.length; i++) {
@@ -123,8 +128,7 @@ export class Personas {
123
128
  const state = this.states.get(recipient.address);
124
129
  if (!state)
125
130
  return false;
126
- const node = this.world.nodeOf(sender, sentCopy);
127
- if (!this.answerable(sender, sentCopy, node))
131
+ if (!this.answerable(sender, sentCopy))
128
132
  return false;
129
133
  const view = this.inboundView(sender, sentCopy, state.received + 1);
130
134
  const hit = this.select(state, view);
@@ -140,13 +144,17 @@ export class Personas {
140
144
  if (!state)
141
145
  return;
142
146
  const node = this.world.nodeOf(mb, msg);
143
- if (sender ? !this.answerable(sender, msg, node) : /mailer-daemon@|postmaster@/i.test(msg.from) || !!headerOf(node, "auto-submitted"))
147
+ if (sender ? !this.answerable(sender, msg) : /mailer-daemon@|postmaster@/i.test(msg.from))
144
148
  return;
149
+ const auto = this.automated(node);
145
150
  state.received++;
146
151
  const view = this.inboundView(mb, msg, state.received);
147
152
  const hit = this.select(state, view);
148
153
  if (!hit)
149
154
  return;
155
+ // An out-of-office never answers a machine (the loop RFC 3834 prevents); a person does.
156
+ if (auto && hit.step.kind === "ooo")
157
+ return;
150
158
  state.consumed.add(hit.index);
151
159
  for (const step of [hit.step, ...(hit.step.also ?? [])])
152
160
  this.respond(mb, view, step);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "esoul-sdk",
3
- "version": "0.21.0",
3
+ "version": "0.21.1",
4
4
  "description": "Build a full product on ExternalSoul: your own tables with per-person rules, a viewer on every seam, app roles, access levels, realtime with audiences, durable tasks, and bindings to other apps.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",