aismemory 0.6.2 → 0.7.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 (36) hide show
  1. package/dist/__tests__/agent-load-persistence.test.js +194 -56
  2. package/dist/__tests__/agent-load-persistence.test.js.map +1 -1
  3. package/dist/__tests__/agent-load-session-override-e2e.test.d.ts +1 -0
  4. package/dist/__tests__/agent-load-session-override-e2e.test.js +212 -0
  5. package/dist/__tests__/agent-load-session-override-e2e.test.js.map +1 -0
  6. package/dist/__tests__/agent-preference.test.d.ts +1 -0
  7. package/dist/__tests__/agent-preference.test.js +84 -0
  8. package/dist/__tests__/agent-preference.test.js.map +1 -0
  9. package/dist/__tests__/device-flow-gate-e2e.test.d.ts +1 -0
  10. package/dist/__tests__/device-flow-gate-e2e.test.js +244 -0
  11. package/dist/__tests__/device-flow-gate-e2e.test.js.map +1 -0
  12. package/dist/__tests__/memory-write-failure.test.js +30 -0
  13. package/dist/__tests__/memory-write-failure.test.js.map +1 -1
  14. package/dist/__tests__/silent-renewal-e2e.test.d.ts +1 -0
  15. package/dist/__tests__/silent-renewal-e2e.test.js +246 -0
  16. package/dist/__tests__/silent-renewal-e2e.test.js.map +1 -0
  17. package/dist/__tests__/token-manager.test.d.ts +1 -0
  18. package/dist/__tests__/token-manager.test.js +253 -0
  19. package/dist/__tests__/token-manager.test.js.map +1 -0
  20. package/dist/__tests__/token-rejection-reauth-e2e.test.d.ts +1 -0
  21. package/dist/__tests__/token-rejection-reauth-e2e.test.js +219 -0
  22. package/dist/__tests__/token-rejection-reauth-e2e.test.js.map +1 -0
  23. package/dist/agent-preference.d.ts +24 -0
  24. package/dist/agent-preference.js +51 -0
  25. package/dist/agent-preference.js.map +1 -0
  26. package/dist/index.js +86 -50
  27. package/dist/index.js.map +1 -1
  28. package/dist/key-auth.d.ts +8 -0
  29. package/dist/key-auth.js +13 -0
  30. package/dist/key-auth.js.map +1 -1
  31. package/dist/telemetry.d.ts +1 -1
  32. package/dist/telemetry.js.map +1 -1
  33. package/dist/token-manager.d.ts +61 -0
  34. package/dist/token-manager.js +179 -0
  35. package/dist/token-manager.js.map +1 -0
  36. package/package.json +1 -1
@@ -0,0 +1,179 @@
1
+ /**
2
+ * TokenManager: owns all in-memory token state for the aismemory client.
3
+ *
4
+ * - First `getCredentials()` authenticates and resolves agent identity;
5
+ * concurrent callers during that first acquire share one in-flight promise.
6
+ * - A renewal is scheduled at 75% of the token's TTL. On success the
7
+ * token/expiresAt swap in place — identity (agentId/tenantId) is resolved
8
+ * once, on the very first acquire, and never re-resolved.
9
+ * - Renewal failures retry with exponential backoff (5s → 10s → 20s ...
10
+ * capped at 300s, ±20% jitter) while the current token remains valid.
11
+ * Backoff state (retryDelayMs/lastAttemptAt/lastError) is shared between
12
+ * the background renewal loop and any on-demand `getCredentials()` call
13
+ * made while the token is expired, so a burst of sequential calls during
14
+ * an outage makes at most one fresh authenticate() attempt per backoff
15
+ * window instead of one per call.
16
+ * - Once the current token hits its hard expiry with renewal still failing,
17
+ * `getCredentials()` rejects with `AisUnreachableError` until the next
18
+ * successful renewal — retries never stop, and recovery needs no restart.
19
+ * - No code path ever serializes the token to disk; it lives only in the
20
+ * closure state below for the process lifetime.
21
+ * - `invalidate()` drops the current token (without touching backoff state)
22
+ * so a caller that discovers the server rejected it mid-session — e.g. a
23
+ * 401 from a JWT_SECRET rotation, CORBOT-A5BBD580 — can force the next
24
+ * `getCredentials()` to acquire a fresh one instead of re-serving the
25
+ * rejected token.
26
+ */
27
+ /**
28
+ * Thrown by `getCredentials()` once the current token has hard-expired and
29
+ * background renewal is still failing. Renewal keeps retrying in the
30
+ * background; the next successful renewal makes `getCredentials()` resolve
31
+ * again with no restart required.
32
+ */
33
+ export class AisUnreachableError extends Error {
34
+ }
35
+ const RENEW_FRACTION = 0.75;
36
+ const BACKOFF_BASE_MS = 5_000;
37
+ const BACKOFF_CAP_MS = 300_000;
38
+ export function createTokenManager(opts) {
39
+ const now = opts.now ?? Date.now;
40
+ let current = null;
41
+ // Tracked separately from `current` so `invalidate()` (which drops
42
+ // `current` to force a fresh acquire) does not also make `acquire()`
43
+ // re-resolve identity — identity is resolved once, on the very first
44
+ // acquire, and never re-resolved for the life of the manager (see module
45
+ // docstring). Set once, alongside `current`, on the first successful
46
+ // acquire; never cleared.
47
+ let resolvedIdentity = null;
48
+ let inFlight = null;
49
+ let timer = null;
50
+ let retryDelayMs = 0;
51
+ let lastAttemptAt = 0;
52
+ let hardExpiryEmitted = false;
53
+ let stopped = false;
54
+ let lastError = '';
55
+ function jitter(ms) {
56
+ return Math.round(ms * (0.8 + Math.random() * 0.4));
57
+ }
58
+ function schedule(delayMs, fn) {
59
+ if (stopped)
60
+ return;
61
+ if (timer)
62
+ clearTimeout(timer);
63
+ timer = setTimeout(fn, delayMs);
64
+ timer.unref?.();
65
+ }
66
+ function scheduleRenewal(expiresAt) {
67
+ const ttlMs = new Date(expiresAt).getTime() - now();
68
+ schedule(Math.max(1_000, Math.floor(ttlMs * RENEW_FRACTION)), () => {
69
+ void renew();
70
+ });
71
+ }
72
+ /**
73
+ * Records a failed authenticate/resolveIdentity attempt into the shared
74
+ * backoff state, regardless of whether it came from the background renewal
75
+ * loop or from an on-demand `getCredentials()` call on an expired token.
76
+ * Escalates retryDelayMs, marks the attempt time (so callers can tell
77
+ * whether a fresh attempt is due), emits `renew.retry`, and — once — the
78
+ * `renew.hard_expiry` event when the current token has passed hard expiry.
79
+ */
80
+ function recordFailure(err) {
81
+ lastError = err instanceof Error ? err.message : String(err);
82
+ lastAttemptAt = now();
83
+ retryDelayMs = retryDelayMs === 0 ? BACKOFF_BASE_MS : Math.min(retryDelayMs * 2, BACKOFF_CAP_MS);
84
+ opts.onEvent?.('renew.retry', lastError);
85
+ if (current && new Date(current.expiresAt).getTime() <= now() && !hardExpiryEmitted) {
86
+ hardExpiryEmitted = true;
87
+ opts.onEvent?.('renew.hard_expiry', lastError);
88
+ }
89
+ }
90
+ async function acquire() {
91
+ const fresh = await opts.authenticate();
92
+ if (!Number.isFinite(new Date(fresh.expiresAt).getTime())) {
93
+ throw new Error(`TokenManager.acquire(): authenticate() returned an invalid expiresAt: ${JSON.stringify(fresh.expiresAt)}`);
94
+ }
95
+ // Identity is resolved only on the very first acquire; every later
96
+ // acquire (renewal, or a re-acquire after invalidate()) reuses the
97
+ // cached agentId/tenantId.
98
+ let agentId;
99
+ let tenantId;
100
+ if (resolvedIdentity) {
101
+ agentId = resolvedIdentity.agentId;
102
+ tenantId = resolvedIdentity.tenantId;
103
+ }
104
+ else {
105
+ const ids = await opts.resolveIdentity(fresh.token);
106
+ agentId = ids.agentId;
107
+ tenantId = ids.tenantId;
108
+ resolvedIdentity = { agentId, tenantId };
109
+ }
110
+ current = { token: fresh.token, expiresAt: fresh.expiresAt, agentId, tenantId };
111
+ retryDelayMs = 0;
112
+ hardExpiryEmitted = false;
113
+ scheduleRenewal(fresh.expiresAt);
114
+ return current;
115
+ }
116
+ async function renew() {
117
+ try {
118
+ await acquire();
119
+ opts.onEvent?.('renew.success');
120
+ }
121
+ catch (err) {
122
+ recordFailure(err);
123
+ schedule(jitter(retryDelayMs), () => {
124
+ void renew();
125
+ });
126
+ }
127
+ }
128
+ return {
129
+ async getCredentials() {
130
+ if (current && new Date(current.expiresAt).getTime() > now()) {
131
+ return current;
132
+ }
133
+ if (!inFlight) {
134
+ // The current token is missing or expired. If a prior attempt
135
+ // (from here or from the background renewal loop) failed and we're
136
+ // still inside its backoff window, don't make a fresh attempt —
137
+ // that would let a burst of sequential calls (e.g. several tool
138
+ // calls in one MCP chat turn) each hammer the auth endpoint during
139
+ // an outage, racing the properly-backed-off background timer and
140
+ // defeating the point of backoff. Fail fast with the last known
141
+ // error instead; the background timer (or the next call after the
142
+ // window elapses) makes the next real attempt.
143
+ if (retryDelayMs > 0 && now() - lastAttemptAt < retryDelayMs) {
144
+ throw new AisUnreachableError(`AIS unreachable — renewing automatically; no action needed. Last error: ${lastError}`);
145
+ }
146
+ inFlight = acquire().finally(() => {
147
+ inFlight = null;
148
+ });
149
+ }
150
+ try {
151
+ return await inFlight;
152
+ }
153
+ catch (err) {
154
+ recordFailure(err);
155
+ throw new AisUnreachableError(`AIS unreachable — renewing automatically; no action needed. Last error: ${lastError}`);
156
+ }
157
+ },
158
+ /**
159
+ * Drops the current token so the next `getCredentials()` must acquire a
160
+ * fresh one — used when the server itself rejects a still-time-valid
161
+ * token (e.g. a mid-session JWT_SECRET rotation, CORBOT-A5BBD580).
162
+ * Deliberately does NOT touch backoff bookkeeping (`retryDelayMs` /
163
+ * `lastAttemptAt`): a server that keeps rejecting tokens must not turn
164
+ * into an unthrottled hammer on the auth endpoint — `recordFailure`'s
165
+ * shared-backoff-window gate in `getCredentials()` still applies as-is.
166
+ * Does not cancel or reschedule the background renewal timer; a
167
+ * successful next acquire reschedules renewal exactly as it does today.
168
+ */
169
+ invalidate() {
170
+ current = null;
171
+ },
172
+ stop() {
173
+ stopped = true;
174
+ if (timer)
175
+ clearTimeout(timer);
176
+ },
177
+ };
178
+ }
179
+ //# sourceMappingURL=token-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token-manager.js","sourceRoot":"","sources":["../src/token-manager.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aismemory",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "description": "Persistent memory for AI agents. MCP server that connects to Agent Identity Service.",
5
5
  "mcpName": "ai.agentsandswarms/ais-memory",
6
6
  "type": "module",