aismemory 0.7.5 → 0.8.2

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 (133) hide show
  1. package/README.md +247 -7
  2. package/dist/__tests__/agent-archive.test.d.ts +1 -0
  3. package/dist/__tests__/agent-archive.test.js +153 -0
  4. package/dist/__tests__/agent-archive.test.js.map +1 -0
  5. package/dist/__tests__/agent-key-mode.test.js +121 -6
  6. package/dist/__tests__/agent-key-mode.test.js.map +1 -1
  7. package/dist/__tests__/agent-load-persistence.test.js +22 -6
  8. package/dist/__tests__/agent-load-persistence.test.js.map +1 -1
  9. package/dist/__tests__/ais-http.test.js +23 -3
  10. package/dist/__tests__/ais-http.test.js.map +1 -1
  11. package/dist/__tests__/auto-handoff.test.js +56 -0
  12. package/dist/__tests__/auto-handoff.test.js.map +1 -1
  13. package/dist/__tests__/bond-link-survives-exit.test.d.ts +1 -0
  14. package/dist/__tests__/bond-link-survives-exit.test.js +177 -0
  15. package/dist/__tests__/bond-link-survives-exit.test.js.map +1 -0
  16. package/dist/__tests__/credential-resolution.test.js +56 -8
  17. package/dist/__tests__/credential-resolution.test.js.map +1 -1
  18. package/dist/__tests__/credential-whoami.test.d.ts +8 -0
  19. package/dist/__tests__/credential-whoami.test.js +93 -0
  20. package/dist/__tests__/credential-whoami.test.js.map +1 -0
  21. package/dist/__tests__/device-flow-gate-e2e.test.js +6 -4
  22. package/dist/__tests__/device-flow-gate-e2e.test.js.map +1 -1
  23. package/dist/__tests__/doctor.test.js +176 -85
  24. package/dist/__tests__/doctor.test.js.map +1 -1
  25. package/dist/__tests__/full-context-view-forwarding.test.d.ts +1 -0
  26. package/dist/__tests__/full-context-view-forwarding.test.js +80 -0
  27. package/dist/__tests__/full-context-view-forwarding.test.js.map +1 -0
  28. package/dist/__tests__/hydration.test.js +56 -1
  29. package/dist/__tests__/hydration.test.js.map +1 -1
  30. package/dist/__tests__/install.test.d.ts +1 -0
  31. package/dist/__tests__/install.test.js +126 -0
  32. package/dist/__tests__/install.test.js.map +1 -0
  33. package/dist/__tests__/integration.test.js +1 -0
  34. package/dist/__tests__/integration.test.js.map +1 -1
  35. package/dist/__tests__/lost-agent.test.js +51 -0
  36. package/dist/__tests__/lost-agent.test.js.map +1 -1
  37. package/dist/__tests__/mcp-tools.test.js +210 -3
  38. package/dist/__tests__/mcp-tools.test.js.map +1 -1
  39. package/dist/__tests__/memory-write-failure.test.js +4 -8
  40. package/dist/__tests__/memory-write-failure.test.js.map +1 -1
  41. package/dist/__tests__/no-registered-keys-disk-bearer-fallback.test.js +4 -7
  42. package/dist/__tests__/no-registered-keys-disk-bearer-fallback.test.js.map +1 -1
  43. package/dist/__tests__/pending-device-flow-e2e.test.js +22 -1
  44. package/dist/__tests__/pending-device-flow-e2e.test.js.map +1 -1
  45. package/dist/__tests__/pending-device-flow-resume.test.d.ts +1 -0
  46. package/dist/__tests__/pending-device-flow-resume.test.js +104 -0
  47. package/dist/__tests__/pending-device-flow-resume.test.js.map +1 -0
  48. package/dist/__tests__/prompt-resource.test.js +1 -0
  49. package/dist/__tests__/prompt-resource.test.js.map +1 -1
  50. package/dist/__tests__/provisional-register-mcp-remember.test.js +4 -8
  51. package/dist/__tests__/provisional-register-mcp-remember.test.js.map +1 -1
  52. package/dist/__tests__/reconnect-survival-e2e.test.js +3 -4
  53. package/dist/__tests__/reconnect-survival-e2e.test.js.map +1 -1
  54. package/dist/__tests__/refresh.test.js +24 -2
  55. package/dist/__tests__/refresh.test.js.map +1 -1
  56. package/dist/__tests__/request-timeout.test.d.ts +1 -0
  57. package/dist/__tests__/request-timeout.test.js +40 -0
  58. package/dist/__tests__/request-timeout.test.js.map +1 -0
  59. package/dist/__tests__/resolve-agent-preference-e2e.test.js +15 -3
  60. package/dist/__tests__/resolve-agent-preference-e2e.test.js.map +1 -1
  61. package/dist/__tests__/retry-after.test.d.ts +4 -0
  62. package/dist/__tests__/retry-after.test.js +134 -0
  63. package/dist/__tests__/retry-after.test.js.map +1 -0
  64. package/dist/__tests__/run-pass-mode.test.d.ts +1 -0
  65. package/dist/__tests__/run-pass-mode.test.js +130 -0
  66. package/dist/__tests__/run-pass-mode.test.js.map +1 -0
  67. package/dist/__tests__/session-identity.test.d.ts +1 -0
  68. package/dist/__tests__/session-identity.test.js +213 -0
  69. package/dist/__tests__/session-identity.test.js.map +1 -0
  70. package/dist/__tests__/version-check.test.js +1 -1
  71. package/dist/__tests__/workspace-key-mode.test.js +194 -16
  72. package/dist/__tests__/workspace-key-mode.test.js.map +1 -1
  73. package/dist/agent-archive.d.ts +36 -0
  74. package/dist/agent-archive.js +69 -0
  75. package/dist/agent-archive.js.map +1 -0
  76. package/dist/agent-key-mode.js +86 -29
  77. package/dist/agent-key-mode.js.map +1 -1
  78. package/dist/ais-http-error.d.ts +7 -2
  79. package/dist/ais-http-error.js +18 -5
  80. package/dist/ais-http-error.js.map +1 -1
  81. package/dist/ais-http.d.ts +2 -0
  82. package/dist/ais-http.js +41 -19
  83. package/dist/ais-http.js.map +1 -1
  84. package/dist/auto-handoff.js +33 -1
  85. package/dist/auto-handoff.js.map +1 -1
  86. package/dist/cli/doctor.d.ts +5 -2
  87. package/dist/cli/doctor.js +72 -15
  88. package/dist/cli/doctor.js.map +1 -1
  89. package/dist/cli/install.d.ts +6 -0
  90. package/dist/cli/install.js +52 -0
  91. package/dist/cli/install.js.map +1 -0
  92. package/dist/cli/orientation.d.ts +2 -0
  93. package/dist/cli/orientation.js +31 -1
  94. package/dist/cli/orientation.js.map +1 -1
  95. package/dist/credential-whoami.d.ts +50 -0
  96. package/dist/credential-whoami.js +91 -0
  97. package/dist/credential-whoami.js.map +1 -0
  98. package/dist/hydration.js +8 -0
  99. package/dist/hydration.js.map +1 -1
  100. package/dist/index.js +213 -50
  101. package/dist/index.js.map +1 -1
  102. package/dist/mcp/tools.d.ts +18 -4
  103. package/dist/mcp/tools.js +107 -12
  104. package/dist/mcp/tools.js.map +1 -1
  105. package/dist/pending-device-flow.d.ts +31 -0
  106. package/dist/pending-device-flow.js +38 -0
  107. package/dist/pending-device-flow.js.map +1 -1
  108. package/dist/refresh.js +5 -1
  109. package/dist/refresh.js.map +1 -1
  110. package/dist/request-timeout.d.ts +26 -0
  111. package/dist/request-timeout.js +49 -0
  112. package/dist/request-timeout.js.map +1 -0
  113. package/dist/retry-after.d.ts +26 -0
  114. package/dist/retry-after.js +42 -0
  115. package/dist/retry-after.js.map +1 -0
  116. package/dist/run-pass-mode.d.ts +16 -0
  117. package/dist/run-pass-mode.js +274 -0
  118. package/dist/run-pass-mode.js.map +1 -0
  119. package/dist/session-identity.d.ts +40 -0
  120. package/dist/session-identity.js +179 -0
  121. package/dist/session-identity.js.map +1 -0
  122. package/dist/telemetry.d.ts +1 -1
  123. package/dist/telemetry.js.map +1 -1
  124. package/dist/tool-args.d.ts +13 -1
  125. package/dist/tool-args.js +27 -1
  126. package/dist/tool-args.js.map +1 -1
  127. package/dist/types.d.ts +32 -0
  128. package/dist/workspace-key-mode.js +100 -26
  129. package/dist/workspace-key-mode.js.map +1 -1
  130. package/package.json +4 -6
  131. package/dist/harness-identity.d.ts +0 -17
  132. package/dist/harness-identity.js +0 -105
  133. package/dist/harness-identity.js.map +0 -1
package/README.md CHANGED
@@ -37,7 +37,7 @@ Add to your IDE's MCP server config:
37
37
  "mcpServers": {
38
38
  "aismemory": {
39
39
  "command": "npx",
40
- "args": ["aismemory@latest"],
40
+ "args": ["aismemory@0.8.2"],
41
41
  "env": {
42
42
  "npm_config_audit": "false",
43
43
  "npm_config_fund": "false"
@@ -47,10 +47,16 @@ Add to your IDE's MCP server config:
47
47
  }
48
48
  ```
49
49
 
50
- Pin `@latest` so a cached `npx` install can't strand you on an older,
51
- possibly-defective release; `npx` re-resolves and downloads whenever `latest`
52
- moves, instead of silently reusing whatever version it last cached under the
53
- bare `aismemory` specifier.
50
+ Pin an exact version rather than `aismemory@latest`. With `@latest`, every
51
+ client startup makes `npx` resolve the tag against the npm registry before it
52
+ can even launch the cached install — on a slow or congested connection that
53
+ resolution call can itself hang, and clients with a startup connect timeout
54
+ (for example Claude Code's 30s `CONNECT_TIMEOUT`) will kill the launch before
55
+ the server ever starts (GitHub issue #6635). A pinned version skips that
56
+ resolution call entirely and reuses the local `npx` cache. The version-check
57
+ module (`AISMEMORY_VERSION_CHECK`) already tells you in-session when a newer
58
+ release is available, so pinning no longer trades away staleness detection —
59
+ bump the pinned version when it does.
54
60
 
55
61
  npm's post-install audit call can stall a cold `npx` start for minutes; the
56
62
  `env` block above skips it and changes nothing about the server itself.
@@ -74,7 +80,8 @@ After bonding, your agent has persistent memory. Every future session signs in o
74
80
  | `remember` | Store a memory (fact, event, lesson, context, goal, task) |
75
81
  | `recall` | Search memories by natural language query |
76
82
  | `whoami` | Get agent identity and status |
77
- | `full_context` | Load complete agent context (personality, goals, facts, skills) |
83
+ | `orientation_check` | Diff an owner-approved expected-environment manifest against this session's evidence — read-only, same comparator as `aismemory doctor --orientation`, reachable without a filesystem |
84
+ | `full_context` | Load complete agent context (personality, goals, facts, skills, and the versioned operating layer — constitution rules plus the skills index) |
78
85
  | `dream` | Trigger memory consolidation (promotes important, archives old) |
79
86
  | `handoff` | Save session context for the next session |
80
87
  | `agent_load` | Switch the active agent by name — triggers on "get me X", "bring me X", "load X", "/agent X", "switch to X" |
@@ -85,6 +92,25 @@ After bonding, your agent has persistent memory. Every future session signs in o
85
92
  | `goal_list` | List goals for the current agent, with status/parent filters |
86
93
  | `goal_update_progress` | Update a goal's progress (0-100) |
87
94
  | `goal_complete` | Mark a goal complete (success or failure) |
95
+ | `agent_archive` | Archive one of your own agents by id. Restorable; nothing is deleted. Owner sign-in only |
96
+ | `agent_delete` | Compatibility alias for `agent_archive`. It archives; it does not delete |
97
+
98
+ ### Archiving an agent you own
99
+
100
+ Archive is the only lifecycle action this package offers. Ask your agent to
101
+ call `agent_archive` with the id of an agent you own:
102
+
103
+ ```
104
+ agent_archive { "agentId": "<agent id>" }
105
+ ```
106
+
107
+ The agent's status becomes `archived` and it stops showing as a live agent.
108
+ Its memories are kept, and you can restore it. `agent_delete` does exactly the
109
+ same thing and says so; the name is kept only so older prompts keep working.
110
+
111
+ Archiving needs your own sign-in (the default, browser-approved mode). The
112
+ service refuses an agent that belongs to someone else. Agent API keys and
113
+ workspace keys cannot archive any agent, their own included.
88
114
 
89
115
  ## Works With
90
116
 
@@ -106,6 +132,8 @@ Authentication uses the OAuth device flow (RFC 8628). Credentials are saved to `
106
132
  | Variable | Default | Description |
107
133
  |----------|---------|-------------|
108
134
  | `AIS_URL` | `https://ais.agentsandswarms.ai` | AIS server URL |
135
+ | `AIS_WORKSPACE_KEY` | unset | Workspace key (`ais_wk_...`) for several agents; see [One key for many agents](#one-key-for-many-agents-ais_workspace_key) |
136
+ | `AIS_MCP_REQUEST_TIMEOUT_MS` | `20000` | Client-side timeout, in milliseconds, for every HTTP call this server makes to AIS. A stalled call fails with a clear error instead of hanging. Must be a positive number; anything else falls back to the default |
109
137
  | `AISMEMORY_VERSION_CHECK` | on | Set to `off` to stop the daily check for a newer published version of this package |
110
138
 
111
139
  ## Unattended machines (agent API keys)
@@ -140,11 +168,140 @@ flow or creates a new agent on your behalf.
140
168
  This credential is intentionally scoped: it can only read/write that one
141
169
  agent's `memory`, `goals`, `dream`, `full_context`, and `handoff` data. Tools
142
170
  that need a broader route — `agent_load` (switching agents, which needs
143
- tenant-wide agent listing) — return an explicit "not available with agent API
171
+ tenant-wide agent listing) and `agent_archive` / `agent_delete` (archiving is
172
+ for the owner sign-in only) — return an explicit "not available with agent API
144
173
  key auth" error rather than failing silently or faking success. `whoami`
145
174
  answers from `AIS_AGENT_ID` directly instead of calling the (unscoped)
146
175
  identity route.
147
176
 
177
+ ## Many agents on one machine
178
+
179
+ Identity is **per session**, never per machine. A session picks its agent at
180
+ launch — `AIS_AGENT_ID` exported in the shell that starts `aismemory`, or set
181
+ in the `env` block of that session's own MCP config — and can switch agents
182
+ in-session with the `agent_load` tool. Run as many sessions as you like side by
183
+ side on one machine, each as a different agent; nothing pins a machine, an
184
+ install, or a harness to one agent.
185
+
186
+ Generate a separate portable MCP config for each agent (use its **agent ID**
187
+ from `whoami`, not its display name; this offline command does not resolve names):
188
+
189
+ ```sh
190
+ aismemory install --identity <agent-a-id> --out ./agent-configs
191
+ aismemory install --identity <agent-b-id> --out ./agent-configs
192
+ ```
193
+
194
+ Each command writes `aismemory-<agent-id>.mcp.json` with that ID in the server's
195
+ `AIS_AGENT_ID` env, and prints a shell/start-script snippet:
196
+
197
+ ```sh
198
+ AIS_AGENT_ID='<agent-a-id>' claude --strict-mcp-config --mcp-config '/absolute/path/agent-configs/aismemory-<agent-a-id>.mcp.json'
199
+ ```
200
+
201
+ The default output directory is `./aismemory-configs`. Existing files are never
202
+ overwritten; use a new output directory to regenerate. The config contains no
203
+ secrets and copies no credentials. Authenticate as needed and run `whoami` in
204
+ each new session. `--strict-mcp-config` selects only this file for the session;
205
+ add other needed MCP servers to it. Other clients can load the generated MCP
206
+ server entry in their **session-specific** config.
207
+
208
+ Never put `AIS_AGENT_ID` in user-scope `~/.claude.json` or a shared shell profile.
209
+ Install does not edit shared config or create legacy harness pins. Doctor warns
210
+ when user-scope Claude env blocks contain an agent ID and explains how to move
211
+ to separate session configs; it never edits or deletes those defaults.
212
+
213
+ Setting `AIS_AGENT_ID` also arms an identity gate: before `remember`,
214
+ `handoff`, `dream`, `inbox_send`, and `inbox_read` with `ack`, the client asks
215
+ `whoami` and **refuses the write** if the live agent is not the one this
216
+ session selected. It refuses a mismatched write; it never pins anything
217
+ outside this process.
218
+
219
+ `aismemory doctor` reports the identity of the process you run it in. Earlier
220
+ 0.7.x builds could install a per-harness pin file under
221
+ `~/.aismemory/harnesses/`; that mechanism is gone, nothing reads those files,
222
+ and doctor only warns if it finds one left behind — delete them yourself when
223
+ you want them gone.
224
+
225
+ ## Unattended credentials
226
+
227
+ Credential mode order is deterministic:
228
+
229
+ 1. `AIS_WORKSPACE_KEY` (or `AIS_API_KEY=ais_wk_...`)
230
+ 2. `AIS_AGENT_API_KEY` (or `AIS_API_KEY=ais_ak_...`)
231
+ 3. `AIS_RUN_PASS_FILE`
232
+ 4. saved sign-in / browser sign-in
233
+
234
+ When `AIS_RUN_PASS_FILE` is unset, explicit keys keep their existing
235
+ precedence over saved sign-in. If `AIS_RUN_PASS_FILE` is set together with
236
+ `AIS_WORKSPACE_KEY`, `AIS_AGENT_API_KEY`, or `AIS_API_KEY`, startup is refused
237
+ instead of silently falling back to a long-lived key.
238
+
239
+ ## One key for many agents (`AIS_WORKSPACE_KEY`)
240
+
241
+ If you run several agents, the owner of those agents can mint one
242
+ **workspace key** (it starts with `ais_wk_`) and grant it to exactly the
243
+ agents it should reach. One connection then serves all of them; you switch
244
+ between them with `agent_load`.
245
+
246
+ How it differs from an agent API key:
247
+
248
+ | | Agent API key | Workspace key |
249
+ |---|---|---|
250
+ | Variable | `AIS_AGENT_API_KEY` | `AIS_WORKSPACE_KEY` |
251
+ | Prefix | `ais_ak_` | `ais_wk_` |
252
+ | Reaches | the one agent it was minted for | every agent its owner granted it, and no others |
253
+ | `agent_load` | not available | switches between granted agents |
254
+ | HTTP header | `x-agent-api-key` | `x-workspace-key` |
255
+
256
+ A workspace key never reaches an agent it was not granted: `agent_load` for
257
+ such an agent fails with a "not granted" error, and the owner has to add that
258
+ agent to the key. Treat the key like a password; if it leaks, revoke it and
259
+ mint a new one.
260
+
261
+ **Local MCP server.** Set the key and the agent the session should start as:
262
+
263
+ ```json
264
+ {
265
+ "mcpServers": {
266
+ "aismemory": {
267
+ "command": "npx",
268
+ "args": ["aismemory@0.8.2"],
269
+ "env": {
270
+ "AIS_WORKSPACE_KEY": "ais_wk_...",
271
+ "AIS_AGENT_ID": "<the agent to start with>",
272
+ "npm_config_audit": "false",
273
+ "npm_config_fund": "false"
274
+ }
275
+ }
276
+ }
277
+ }
278
+ ```
279
+
280
+ `AIS_AGENT_ID` is required with a workspace key. If it is missing, or the key
281
+ does not start with `ais_wk_`, the server prints the reason and exits instead
282
+ of falling back to the browser sign-in. `AIS_WORKSPACE_KEY` wins over
283
+ `AIS_AGENT_API_KEY` when both are set.
284
+
285
+ ## One run pass for one run (`AIS_RUN_PASS_FILE`)
286
+
287
+ Launchers can set `AIS_RUN_PASS_FILE` to the path of a file containing a
288
+ short-lived run pass (`ais_rp_...`) and `AIS_AGENT_ID` to the agent for that
289
+ run. On every AIS request, aismemory re-reads the file and sends only:
290
+
291
+ ```http
292
+ x-run-pass: ais_rp_...
293
+ ```
294
+
295
+ It never sends a workspace key, agent key, bearer token, or service key in
296
+ run-pass mode. If the file is missing, unreadable, empty, or does not contain
297
+ a valid run pass, the request is refused locally and the pass value is never
298
+ printed.
299
+
300
+ **Hosted endpoint.** Clients that connect to
301
+ `https://ais.agentsandswarms.ai/mcp` directly send the key in the
302
+ `X-Workspace-Key` header instead of signing in. The session starts with no
303
+ active agent: call `agent_load` with one of the granted agents first.
304
+
148
305
  ## Backing up memories to a local file
149
306
 
150
307
  `aismemory pull` copies this agent's memories out of Agent Identity Service and
@@ -172,6 +329,73 @@ reads only the memories of the agent named in `AIS_AGENT_ID`. It works with a
172
329
  scoped agent API key — no broader credential is needed and nothing is written
173
330
  back to the service.
174
331
 
332
+ ## Tags
333
+
334
+ `remember` accepts `tags` (for example `["work", "acme"]`) and `recall` accepts
335
+ `tags` to return only memories carrying all of them. Over REST:
336
+
337
+ ```
338
+ POST /v1/agents/<agent id>/memory {"content": "...", "type": "fact", "tags": ["work", "acme"]}
339
+ PATCH /v1/agents/<agent id>/memory/<memory id> {"tags": ["work"]} (replaces the set; [] clears it)
340
+ GET /v1/agents/<agent id>/memory?query=...&tags=work,acme (carries every tag)
341
+ GET /v1/agents/<agent id>/memory?anyTags=work,acme (carries at least one)
342
+ ```
343
+
344
+ - At most 20 tags per memory, each 1-64 characters of `a-z 0-9 . _ : -`. Tags
345
+ are trimmed and lower-cased; duplicates are dropped; the `ais:` prefix is
346
+ reserved. Anything else is a 400 that says which tag is wrong.
347
+ - Set tags with the `tags` field only. A `tags` key inside `metadata` is
348
+ ignored, and replacing `metadata` keeps the memory's tags.
349
+ - Every memory in a response carries `tags` (`[]` when it has none).
350
+ - The filter applies before ranking, on list and recall, with or without
351
+ `query`, and with cursor paging. A recall never widens past it: few or zero
352
+ results is the answer when few memories carry the tags.
353
+ - Tags are a filter, not a permission. Anyone who can read the agent can read
354
+ every memory and every tag. To keep work and personal memories truly
355
+ apart, use two agents.
356
+
357
+ ## Listing and paging memories over REST
358
+
359
+ `recall` is a ranked search. To list every memory, call the REST route without
360
+ a query and follow the cursor:
361
+
362
+ ```
363
+ GET /v1/agents/<agent id>/memory?sortBy=createdAt&order=desc&limit=100
364
+ GET /v1/agents/<agent id>/memory?limit=100&cursor=<data.nextCursor from the previous page>
365
+ ```
366
+
367
+ - `limit` is 1-100. Asking for more returns a 400 that names the maximum.
368
+ - Every page ordered by `createdAt` or `updatedAt` carries `data.nextCursor`.
369
+ Pass it back unchanged as `cursor`; stop when it is `null`. The cursor
370
+ remembers the order, so memories written while you page never make a page
371
+ repeat or skip a row. Send the same filters (`types`, `tiers`, ...) on every
372
+ page.
373
+ - Every memory carries `updatedAt`: the time of its last edit (content, type,
374
+ metadata, importance, goal, or being superseded). Recalling a memory does
375
+ not change it. A memory never edited has `updatedAt` equal to `createdAt`.
376
+ - `sortBy=updatedAt` lists the most recently edited memories first
377
+ (`order=desc`) — the way to pick up changes since your last sync. A memory
378
+ edited while you page moves to the newest end: an `order=desc` walk will not
379
+ see that edit (start a new walk to pick it up), and an `order=asc` walk
380
+ serves it again on a later page.
381
+ - `cursor` cannot be combined with `query`, `offset` or `page`. `offset`/`page`
382
+ still work, but can serve a row twice when memories are written mid-walk.
383
+ - Superseded memories (replaced by a newer one) are hidden unless you pass
384
+ `includeSuperseded=true`; each memory's `temporal.supersededBy` and
385
+ `temporal.isCurrentFact` say which is which. Expired memories need
386
+ `includeExpired=true`.
387
+ - `data.totalCount` counts every matching memory, not the ones still to come.
388
+
389
+ ## When AIS is busy
390
+
391
+ Every `429` (rate limit or quota) and `503` (overloaded or briefly
392
+ unavailable) from AIS carries a `Retry-After` header in seconds. aismemory
393
+ reads it: a read that is asked to wait 5 seconds or less waits and tries once
394
+ more on its own; anything longer, and every write, fails right away with an
395
+ error that says how many seconds AIS asked you to wait. Wait that long, then
396
+ run the same call again. Writes are never retried automatically, so a memory
397
+ is never stored twice by a retry.
398
+
175
399
  ## Troubleshooting
176
400
 
177
401
  If aismemory "doesn't work," run the built-in self-diagnosis before filing a
@@ -188,6 +412,22 @@ with a PASS/WARN/FAIL line and a remediation hint. Nothing is written to the
188
412
  API. Pass `--json` for machine-readable output. Exits `0` unless a check
189
413
  FAILs.
190
414
 
415
+ ## Source and provenance
416
+
417
+ Source is private. This package is published from GitHub Actions via npm
418
+ Trusted Publishing (OIDC) — no long-lived token, and the publish is bound to
419
+ this project's own CI workflow (verify with `npm view aismemory --json` and
420
+ check `_npmUser.trustedPublisher`). Every package on npm, including this one,
421
+ carries npm's registry signature, checkable locally with `npm audit
422
+ signatures`. That is separate from npm's build provenance/attestation
423
+ feature (a public, source-linked attestation), which requires a public
424
+ source repository and is not enabled here while the source stays private.
425
+
426
+ Official domains: https://www.agentsandswarms.ai and
427
+ https://ais.agentsandswarms.ai. Security contact: security@agentsandswarms.ai.
428
+ The human bonding link above (`agentsandswarms.ai/activate`) is issued at
429
+ runtime by Agent Identity Service, not hardcoded in this package.
430
+
191
431
  ## License
192
432
 
193
433
  MIT
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,153 @@
1
+ /**
2
+ * agent_archive and the agent_delete alias in the published package
3
+ * (CORBOT-BA5AADB0, John record 2f8a3a49: archive-your-own only).
4
+ *
5
+ * Pinned here:
6
+ * - under the owner sign-in, agent_archive sends
7
+ * PATCH /v1/agents/<id> {"status":"archived"} with the session bearer, and
8
+ * the response says "archived";
9
+ * - agent_delete does exactly the same and says it is an alias that archives;
10
+ * - an agent API key or workspace key cannot archive any agent — the call
11
+ * fails before any request leaves the process;
12
+ * - the tool descriptions say archive, never delete.
13
+ */
14
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
15
+ import { ActivityTracker } from '../auto-handoff.js';
16
+ import { createOAuthCallToolHandler } from '../mcp/tools.js';
17
+ import { dispatchToolKeyMode, OutOfScopeToolError } from '../agent-key-mode.js';
18
+ import { dispatchToolWorkspaceKeyMode } from '../workspace-key-mode.js';
19
+ import { AGENT_ARCHIVE_DESCRIPTION, AGENT_DELETE_ALIAS_DESCRIPTION, archiveAgent, } from '../agent-archive.js';
20
+ const aisGet = vi.hoisted(() => vi.fn());
21
+ const aisPost = vi.hoisted(() => vi.fn());
22
+ const aisPatch = vi.hoisted(() => vi.fn());
23
+ vi.mock('../ais-http.js', () => ({ aisGet, aisPost, aisPatch }));
24
+ const SESSION_AGENT = '11111111-1111-4111-8111-111111111111';
25
+ const LEFTOVER = '22222222-2222-4222-8222-222222222222';
26
+ const CREDS = {
27
+ token: 'owner-token',
28
+ agentId: SESSION_AGENT,
29
+ tenantId: 'tenant-1',
30
+ expiresAt: new Date(Date.now() + 3_600_000).toISOString(),
31
+ };
32
+ function call(name, args) {
33
+ return { method: 'tools/call', params: { name, arguments: args } };
34
+ }
35
+ function makeOwnerHandler() {
36
+ let creds = { ...CREDS };
37
+ const serverStub = { notification: async () => undefined };
38
+ const ctx = {
39
+ aisUrl: 'https://ais.test',
40
+ // The archive path never touches the server; only notification() exists.
41
+ server: serverStub,
42
+ activityTracker: new ActivityTracker(),
43
+ getSessionIdentity: () => null,
44
+ setSessionIdentity: () => { },
45
+ getCredentials: () => creds,
46
+ setCredentials: (next) => { creds = next; },
47
+ setSessionAgentOverride: () => undefined,
48
+ getAgentContext: () => null,
49
+ setAgentContext: () => undefined,
50
+ ensureCredentials: async () => { creds = creds ?? { ...CREDS }; return creds; },
51
+ invalidateToken: () => undefined,
52
+ applyMemoryRefresh: () => undefined,
53
+ resolveAgentForLoad: async () => ({ id: SESSION_AGENT, name: 'Desk', status: 'active' }),
54
+ persistClaudeSessionAgentName: () => undefined,
55
+ isBondPendingError: () => false,
56
+ stderr: () => undefined,
57
+ };
58
+ return createOAuthCallToolHandler(ctx);
59
+ }
60
+ function parse(res) {
61
+ return JSON.parse(res.content[0]?.text ?? '{}');
62
+ }
63
+ describe('agent_archive under the owner sign-in', () => {
64
+ beforeEach(() => {
65
+ aisGet.mockReset();
66
+ aisPost.mockReset();
67
+ aisPatch.mockReset();
68
+ aisPatch.mockResolvedValue({ success: true, data: { id: LEFTOVER, status: 'archived' } });
69
+ });
70
+ it('archives the named agent with PATCH status=archived and the session bearer', async () => {
71
+ const res = await makeOwnerHandler()(call('agent_archive', { agentId: LEFTOVER }));
72
+ expect(res.isError).toBeUndefined();
73
+ expect(aisPatch).toHaveBeenCalledTimes(1);
74
+ expect(aisPatch).toHaveBeenCalledWith(`/v1/agents/${LEFTOVER}`, { status: 'archived' }, 'owner-token');
75
+ expect(aisPost).not.toHaveBeenCalled();
76
+ });
77
+ it('says "archived" and never reports a delete', async () => {
78
+ const body = parse(await makeOwnerHandler()(call('agent_archive', { agentId: LEFTOVER })));
79
+ expect(body).toEqual({
80
+ success: true,
81
+ archived: true,
82
+ agentId: LEFTOVER,
83
+ status: 'archived',
84
+ message: expect.stringMatching(/archived/),
85
+ });
86
+ expect(String(body['message'])).not.toMatch(/agent (was )?deleted|soft delete/i);
87
+ });
88
+ it('agent_delete is an alias: the same request, the same result, plus a note that it archived', async () => {
89
+ const handler = makeOwnerHandler();
90
+ const archived = parse(await handler(call('agent_archive', { agentId: LEFTOVER })));
91
+ const aliased = parse(await handler(call('agent_delete', { agentId: LEFTOVER })));
92
+ expect(aisPatch.mock.calls[1]).toEqual(aisPatch.mock.calls[0]);
93
+ const { aliasOf, note, ...aliasCore } = aliased;
94
+ expect(aliasCore).toEqual(archived);
95
+ expect(aliasOf).toBe('agent_archive');
96
+ expect(note).toMatch(/alias for agent_archive/);
97
+ expect(note).toMatch(/archived/);
98
+ });
99
+ it.each(['agent_archive', 'agent_delete'])('%s refuses a missing or malformed agentId without a request', async (tool) => {
100
+ const handler = makeOwnerHandler();
101
+ const missing = await handler(call(tool, {}));
102
+ const malformed = await handler(call(tool, { agentId: 'not-a-uuid' }));
103
+ expect(missing.isError).toBe(true);
104
+ expect(malformed.isError).toBe(true);
105
+ expect(parse(malformed)['error']).toMatch(/valid agent UUID/);
106
+ expect(aisPatch).not.toHaveBeenCalled();
107
+ });
108
+ it('surfaces a server refusal as an error, not as "archived"', async () => {
109
+ aisPatch.mockRejectedValue(new Error('AIS PATCH /v1/agents/x failed (HTTP 403): This agent is bonded to a different user'));
110
+ const res = await makeOwnerHandler()(call('agent_archive', { agentId: LEFTOVER }));
111
+ expect(res.isError).toBe(true);
112
+ expect(parse(res)['error']).toMatch(/403/);
113
+ expect(parse(res)).not.toHaveProperty('archived');
114
+ });
115
+ it('passes a token-rejection body through so the re-auth retry still fires', async () => {
116
+ const rejected = { success: false, error: { code: 'INVALID_TOKEN', message: 'expired' } };
117
+ await expect(archiveAgent('agent_archive', { agentId: LEFTOVER }, async () => rejected))
118
+ .resolves.toBe(rejected);
119
+ });
120
+ });
121
+ describe('key modes cannot archive any agent', () => {
122
+ function neverFetch() {
123
+ return vi.fn(async () => {
124
+ throw new Error('no request may leave the process');
125
+ });
126
+ }
127
+ it.each(['agent_archive', 'agent_delete'])('agent API key: %s is out of scope, for another agent or its own', async (tool) => {
128
+ const fetchImpl = neverFetch();
129
+ const config = { apiKey: 'ais_ak_test', agentId: SESSION_AGENT };
130
+ for (const target of [LEFTOVER, SESSION_AGENT]) {
131
+ await expect(dispatchToolKeyMode('https://ais.test', config, tool, { agentId: target }, fetchImpl))
132
+ .rejects.toThrow(OutOfScopeToolError);
133
+ }
134
+ expect(fetchImpl).not.toHaveBeenCalled();
135
+ });
136
+ it.each(['agent_archive', 'agent_delete'])('workspace key: %s is out of scope', async (tool) => {
137
+ const fetchImpl = neverFetch();
138
+ const config = { workspaceKey: 'ais_wk_test', agentId: SESSION_AGENT };
139
+ await expect(dispatchToolWorkspaceKeyMode('https://ais.test', config, tool, { agentId: LEFTOVER }, fetchImpl))
140
+ .rejects.toThrow(/owner sign-in/);
141
+ expect(fetchImpl).not.toHaveBeenCalled();
142
+ });
143
+ });
144
+ describe('tool descriptions', () => {
145
+ it('agent_archive describes an archive, not a delete', () => {
146
+ expect(AGENT_ARCHIVE_DESCRIPTION).toMatch(/^Archive one of your own agents/);
147
+ expect(AGENT_ARCHIVE_DESCRIPTION).toMatch(/Nothing is deleted/);
148
+ });
149
+ it('agent_delete says it is an alias that archives', () => {
150
+ expect(AGENT_DELETE_ALIAS_DESCRIPTION).toMatch(/^Compatibility alias for agent_archive\. It ARCHIVES the agent; it does not delete it\./);
151
+ });
152
+ });
153
+ //# sourceMappingURL=agent-archive.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-archive.test.js","sourceRoot":"","sources":["../../src/__tests__/agent-archive.test.ts"],"names":[],"mappings":"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"}
@@ -10,7 +10,8 @@
10
10
  * hitting the network; a 403 from the server is surfaced, not swallowed.
11
11
  */
12
12
  import { describe, expect, it, vi } from 'vitest';
13
- import { AgentApiKeyConfigError, dispatchToolKeyMode, loadAgentApiKeyConfig, OutOfScopeToolError, UNKNOWN_TOOL_KEY_MODE, } from '../agent-key-mode.js';
13
+ import { AisHttpError } from '../ais-http-error.js';
14
+ import { AgentApiKeyConfigError, aisKeyGet, dispatchToolKeyMode, loadAgentApiKeyConfig, OutOfScopeToolError, UNKNOWN_TOOL_KEY_MODE, } from '../agent-key-mode.js';
14
15
  const AIS_URL = 'https://ais.example.test';
15
16
  function jsonResponse(status, body) {
16
17
  return new Response(JSON.stringify(body), {
@@ -95,6 +96,22 @@ describe('dispatchToolKeyMode — in-scope tools send the scoped header only', (
95
96
  const [url] = fetchMock.mock.calls[0];
96
97
  expect(String(url)).toContain('/v1/agents/agent-42/memory?query=test');
97
98
  });
99
+ it('remember and recall pass tags through (Jaire C5)', async () => {
100
+ const fetchMock = vi.fn(async (_url, _init) => jsonResponse(200, { success: true, data: { memories: [] } }));
101
+ await dispatchToolKeyMode(AIS_URL, config, 'remember', { content: 'x', tags: ['work', 'acme'] }, fetchMock);
102
+ await dispatchToolKeyMode(AIS_URL, config, 'recall', { query: 'x', tags: ['work', 'acme'] }, fetchMock);
103
+ await dispatchToolKeyMode(AIS_URL, config, 'remember', { content: 'y' }, fetchMock);
104
+ const body = JSON.parse(String(fetchMock.mock.calls[0][1]?.body));
105
+ expect(body['tags']).toEqual(['work', 'acme']);
106
+ expect(String(fetchMock.mock.calls[1][0])).toContain('tags=work%2Cacme');
107
+ const untagged = JSON.parse(String(fetchMock.mock.calls[2][1]?.body));
108
+ expect(untagged).not.toHaveProperty('tags');
109
+ });
110
+ it('rejects a non-array tags argument before any request (Jaire C5)', async () => {
111
+ const fetchMock = vi.fn();
112
+ await expect(dispatchToolKeyMode(AIS_URL, config, 'remember', { content: 'x', tags: 'work' }, fetchMock)).rejects.toThrow(/tags/);
113
+ expect(fetchMock).not.toHaveBeenCalled();
114
+ });
98
115
  it('full_context: GETs /v1/agents/:id/full-context', async () => {
99
116
  const fetchMock = vi.fn(async (_url, _init) => jsonResponse(200, { success: true, data: {} }));
100
117
  await dispatchToolKeyMode(AIS_URL, config, 'full_context', {}, fetchMock);
@@ -124,22 +141,120 @@ describe('dispatchToolKeyMode — in-scope tools send the scoped header only', (
124
141
  const [url] = fetchMock.mock.calls[0];
125
142
  expect(url).toBe(`${AIS_URL}/v1/agents/agent-42/memory`);
126
143
  });
127
- it('whoami: answers from AIS_AGENT_ID and probes an allowed route (no identity route call)', async () => {
144
+ it('whoami: asks the self-description route and passes its fields through (CORBOT-49535E96)', async () => {
128
145
  const calledPaths = [];
129
146
  const fetchMock = vi.fn(async (url) => {
130
147
  calledPaths.push(String(url));
131
- return jsonResponse(200, { success: true, data: { memories: [] } });
148
+ return jsonResponse(200, {
149
+ success: true,
150
+ data: {
151
+ agentId: 'agent-42',
152
+ tenantId: 'tenant-1',
153
+ mode: 'agent-api-key',
154
+ scopeModel: 'allowlist',
155
+ scopes: ['memory', 'whoami'],
156
+ restrictions: ['cannot mint, list or revoke API keys'],
157
+ actsAsGrantedAgent: false,
158
+ expiresAt: '2027-01-01T00:00:00.000Z',
159
+ },
160
+ });
132
161
  });
133
162
  const result = await dispatchToolKeyMode(AIS_URL, config, 'whoami', {}, fetchMock);
134
- expect(calledPaths).toHaveLength(1);
135
- expect(calledPaths[0]).toContain('/v1/agents/agent-42/memory?limit=1');
163
+ expect(calledPaths).toEqual([`${AIS_URL}/v1/agents/agent-42/whoami`]);
136
164
  // Must never call the bare identity route — it has no allowlisted subresource.
137
165
  expect(calledPaths.some((p) => /\/v1\/agents\/agent-42$/.test(p))).toBe(false);
138
166
  expect(result).toMatchObject({
139
167
  success: true,
140
- data: { agentId: 'agent-42', authMode: 'agent-api-key', keyValid: true },
168
+ data: {
169
+ agentId: 'agent-42',
170
+ authMode: 'agent-api-key',
171
+ keyValid: true,
172
+ selfDescribed: true,
173
+ scopes: ['memory', 'whoami'],
174
+ expiresAt: '2027-01-01T00:00:00.000Z',
175
+ },
176
+ });
177
+ });
178
+ it('whoami: an older server without the route still gets the liveness probe answer', async () => {
179
+ const calledPaths = [];
180
+ const fetchMock = vi.fn(async (url) => {
181
+ calledPaths.push(String(url));
182
+ return String(url).endsWith('/whoami')
183
+ ? jsonResponse(403, { success: false, error: { code: 'FORBIDDEN', message: 'scoped' } })
184
+ : jsonResponse(200, { success: true, data: { memories: [] } });
185
+ });
186
+ const result = await dispatchToolKeyMode(AIS_URL, config, 'whoami', {}, fetchMock);
187
+ expect(calledPaths).toHaveLength(2);
188
+ expect(calledPaths[1]).toContain('/v1/agents/agent-42/memory?limit=1');
189
+ expect(result).toMatchObject({
190
+ success: true,
191
+ data: { agentId: 'agent-42', authMode: 'agent-api-key', keyValid: true, selfDescribed: false },
141
192
  });
142
193
  });
194
+ it('orientation_check: evaluates locally, without any fetch call', async () => {
195
+ const fetchMock = vi.fn();
196
+ const manifest = {
197
+ version: 1,
198
+ identity: '00000000-0000-4000-8000-000000000001',
199
+ requirements: { host: 'cloud-vm', account: 'fleet-account', channels: ['ais'] },
200
+ current: { host: 'cloud-vm', account: 'fleet-account', session: 'agent-key-1' },
201
+ evidence: {
202
+ session: 'agent-key-1', host: 'cloud-vm', account: 'fleet-account',
203
+ credentials: 'pass', credentialSource: 'ais-agent-key',
204
+ channels: { ais: 'pass', mcp: 'missing', inbox: 'missing' },
205
+ },
206
+ relocation: { verified: false },
207
+ };
208
+ const result = await dispatchToolKeyMode(AIS_URL, config, 'orientation_check', manifest, fetchMock);
209
+ expect(fetchMock).not.toHaveBeenCalled();
210
+ expect(result).toMatchObject({ scope: 'public-orientation', exitCode: 0 });
211
+ });
212
+ });
213
+ describe('aisKeyGet retries once on a transient 502/503 (CORBOT-6F45EDFA)', () => {
214
+ // Mirrors ais-http.test.ts's aisGet retry coverage (CORBOT-9492F31A):
215
+ // the same idempotent-GET reasoning applies to x-agent-api-key GETs.
216
+ it('bounds scoped-key GETs with an abort signal so whoami cannot hang indefinitely', async () => {
217
+ const fetchMock = vi.fn(async () => jsonResponse(200, { success: true, data: { id: 'agent-42' } }));
218
+ await expect(aisKeyGet(AIS_URL, '/v1/agents/agent-42/whoami', 'ais_ak_scoped', fetchMock)).resolves.toEqual({ success: true, data: { id: 'agent-42' } });
219
+ const [, init] = fetchMock.mock.calls[0];
220
+ expect(init?.signal).toBeInstanceOf(AbortSignal);
221
+ });
222
+ it('times out with a clear error instead of hanging when AIS never responds (issue #6635)', async () => {
223
+ process.env['AIS_MCP_REQUEST_TIMEOUT_MS'] = '20';
224
+ const hangingFetch = vi.fn((_url, init) => new Promise((_resolve, reject) => {
225
+ init?.signal?.addEventListener('abort', () => reject(init.signal.reason));
226
+ }));
227
+ await expect(aisKeyGet(AIS_URL, '/v1/agents/agent-42/whoami', 'ais_ak_scoped', hangingFetch)).rejects.toThrow('AIS request to /v1/agents/agent-42/whoami timed out after 20ms');
228
+ delete process.env['AIS_MCP_REQUEST_TIMEOUT_MS'];
229
+ });
230
+ it('retries once on a 502 and returns the retry success', async () => {
231
+ const successBody = { success: true, data: { id: 'agent-42' } };
232
+ const fetchMock = vi.fn();
233
+ fetchMock
234
+ .mockResolvedValueOnce(new Response('upstream error', { status: 502 }))
235
+ .mockResolvedValueOnce(jsonResponse(200, successBody));
236
+ await expect(aisKeyGet(AIS_URL, '/v1/agents/agent-42', 'ais_ak_scoped', fetchMock)).resolves.toEqual(successBody);
237
+ expect(fetchMock).toHaveBeenCalledTimes(2);
238
+ });
239
+ it('retries once on a 503 and returns the retry success', async () => {
240
+ const successBody = { success: true, data: { id: 'agent-42' } };
241
+ const fetchMock = vi.fn();
242
+ fetchMock
243
+ .mockResolvedValueOnce(new Response('service unavailable', { status: 503 }))
244
+ .mockResolvedValueOnce(jsonResponse(200, successBody));
245
+ await expect(aisKeyGet(AIS_URL, '/v1/agents/agent-42', 'ais_ak_scoped', fetchMock)).resolves.toEqual(successBody);
246
+ expect(fetchMock).toHaveBeenCalledTimes(2);
247
+ });
248
+ it('only retries once — a second 502 still throws AisHttpError', async () => {
249
+ const fetchMock = vi.fn(async () => jsonResponse(502, { error: { code: 'UPSTREAM', message: 'upstream error' } }));
250
+ await expect(aisKeyGet(AIS_URL, '/v1/agents/agent-42', 'ais_ak_scoped', fetchMock)).rejects.toBeInstanceOf(AisHttpError);
251
+ expect(fetchMock).toHaveBeenCalledTimes(2);
252
+ });
253
+ it('does not retry non-retryable statuses (e.g. 500)', async () => {
254
+ const fetchMock = vi.fn(async () => jsonResponse(500, { error: { code: 'INTERNAL', message: 'server error' } }));
255
+ await expect(aisKeyGet(AIS_URL, '/v1/agents/agent-42', 'ais_ak_scoped', fetchMock)).rejects.toBeInstanceOf(AisHttpError);
256
+ expect(fetchMock).toHaveBeenCalledTimes(1);
257
+ });
143
258
  });
144
259
  describe('dispatchToolKeyMode — inbox tools (CORBOT-7115B8AE, in-scope per CORBOT-04DABA89)', () => {
145
260
  const config = { apiKey: 'ais_ak_scoped', agentId: 'agent-42' };