myagentmemory 0.4.14 → 0.4.15

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/README.md CHANGED
@@ -30,7 +30,7 @@ AgentMemory is free, open-source software under the MIT License. The core is a l
30
30
 
31
31
  ### Optional official plugins
32
32
 
33
- The MIT-licensed core can discover and host separately distributed, signed first-party plugins while remaining fully useful on its own. Optional plugins may use their own license and distribution terms; their implementation and browser assets are not part of the `myagentmemory` package. `agent-memory plugin list` and `plugin status` report local state. In an interactive terminal, `agent-memory plugin install` opens a nonce-bound loopback page for an email address, resumes the waiting command, verifies signed release metadata and the downloaded bundle, and installs atomically. This temporary beta grants unlimited local use. The email remains in a mode-0600 local activation record and is also sent with bounded activation metadata to the private commercial service; memory, sessions, queries, repository paths, IP addresses, and user-agent strings are not stored in the activation database. Authentication and payment will replace this temporary flow later. See the [official plugin bootstrap and host contract](docs/official-plugin-bootstrap.md).
33
+ The MIT-licensed core can discover and host separately distributed, signed first-party plugins while remaining fully useful on its own. Optional plugins may use their own license and distribution terms; their implementation and browser assets are not part of the `myagentmemory` package. `agent-memory plugin list` and `plugin status` report local state. In an interactive terminal, `agent-memory plugin install` opens a nonce-bound loopback page for an email address, resumes the waiting command, verifies signed release metadata and the downloaded bundle, and installs atomically. The free plan uses a configurable daily agent-session allowance keyed by normalized email. D1 stores bounded activation metadata, a credential hash, and opaque SessionStart usage operations; it never receives memory, session content, queries, repository paths, raw agent session identifiers, IP addresses, or user-agent strings. Authentication and payment can extend this free activation flow later. See the [official plugin bootstrap and host contract](docs/official-plugin-bootstrap.md).
34
34
 
35
35
  ## Installation
36
36
 
package/dist/cli.js CHANGED
@@ -178,7 +178,7 @@ function printPluginResult(result, json, allowBrowser) {
178
178
  console.log("Run: agent-memory plugin install");
179
179
  break;
180
180
  case "auth_required":
181
- console.log("Run this command in an interactive terminal to enter an email and activate temporary access.");
181
+ console.log("Run this command in an interactive terminal to enter an email and activate free daily access.");
182
182
  break;
183
183
  case "renewal_required":
184
184
  console.log("Renew AgentMemory Pro to continue using paid capabilities.");
@@ -893,8 +893,8 @@ Usage:
893
893
  agent-memory plugin manage [--no-browser]
894
894
 
895
895
  The public core remains fully usable without AgentMemory Pro. Interactive install
896
- opens a loopback website for temporary email activation and unlimited local use.
897
- Authentication and payment will be added later.`);
896
+ opens a loopback website for email activation and a configurable free daily
897
+ agent-session allowance. Memory and session content stay on this device.`);
898
898
  }
899
899
  function pluginCommandFailure(command, error) {
900
900
  return {
@@ -1099,6 +1099,18 @@ async function main() {
1099
1099
  if (!agent)
1100
1100
  exitError("hook session-start requires --agent", json);
1101
1101
  await cmdContext({ "no-search": true });
1102
+ try {
1103
+ const decision = await new InstalledPluginRuntimeV1({ coreVersion: VERSION }).runSessionStart({
1104
+ host: agent,
1105
+ cwd: process.cwd(),
1106
+ signal: new AbortController().signal,
1107
+ });
1108
+ if (decision?.state === "exhausted")
1109
+ console.error(`AgentMemory free session allowance resets at ${decision.resetAt}`);
1110
+ }
1111
+ catch {
1112
+ // Paid SessionStart work must never make public-core context unavailable.
1113
+ }
1102
1114
  break;
1103
1115
  }
1104
1116
  case "plugin":
@@ -13,6 +13,15 @@ export interface PluginNextActionV1 {
13
13
  userCode?: string;
14
14
  message?: string;
15
15
  }
16
+ export interface PluginSessionUsageDecisionV1 {
17
+ allowed: boolean;
18
+ state: "reserved" | "committed" | "released" | "exhausted" | "missing";
19
+ limit: number;
20
+ used: number;
21
+ remaining: number;
22
+ resetAt: string;
23
+ idempotent: boolean;
24
+ }
16
25
  export interface PluginInstallReceiptV1 {
17
26
  schemaVersion: 1;
18
27
  bundleId: string;
@@ -105,6 +114,9 @@ export interface PluginBootstrapBackendV1 {
105
114
  release: SignedPluginReleaseV1;
106
115
  artifactGrant: string;
107
116
  }): Promise<Uint8Array>;
117
+ reserveSession?(operationId: string): Promise<PluginSessionUsageDecisionV1>;
118
+ commitSession?(operationId: string): Promise<PluginSessionUsageDecisionV1>;
119
+ releaseSession?(operationId: string): Promise<PluginSessionUsageDecisionV1>;
108
120
  getManagementAction(): Promise<PluginNextActionV1 | null>;
109
121
  }
110
122
  export interface PluginReleaseVerifierV1 {
@@ -5,7 +5,7 @@ export type PluginPlanV1 = "free" | "trial" | "pro" | "team" | "enterprise";
5
5
  export interface PluginCapabilityQuotaV1 {
6
6
  limit: number;
7
7
  window: "day";
8
- scope: "device";
8
+ scope: "device" | "account";
9
9
  }
10
10
  export interface PluginCapabilityGrantV1 {
11
11
  enabled: boolean;
@@ -61,7 +61,7 @@ export function validatePluginEntitlementStatusV1(entitlement) {
61
61
  if (grant.quota) {
62
62
  if (!Number.isInteger(grant.quota.limit) || grant.quota.limit <= 0)
63
63
  throw new Error(`Capability ${capability} has an invalid quota limit`);
64
- if (grant.quota.window !== "day" || grant.quota.scope !== "device")
64
+ if (grant.quota.window !== "day" || !["device", "account"].includes(grant.quota.scope))
65
65
  throw new Error(`Capability ${capability} has an invalid quota policy`);
66
66
  }
67
67
  }
@@ -1,5 +1,5 @@
1
- import { type PluginBootstrapBackendV1, type PluginInstallStoreV1, type SignedPluginReleaseV1 } from "./plugin-bootstrap.js";
2
- import { type PluginCommandContextV1, type PluginCommandResultV1 } from "./plugin-host.js";
1
+ import { type PluginBootstrapBackendV1, type PluginInstallStoreV1, type PluginSessionUsageDecisionV1, type SignedPluginReleaseV1 } from "./plugin-bootstrap.js";
2
+ import { type PluginCommandContextV1, type PluginCommandResultV1, type PluginSessionStartHookV1 } from "./plugin-host.js";
3
3
  export interface PluginRuntimeOptionsV1 {
4
4
  coreVersion: string;
5
5
  store?: PluginInstallStoreV1;
@@ -15,6 +15,7 @@ export declare class InstalledPluginRuntimeV1 {
15
15
  constructor(options: PluginRuntimeOptionsV1);
16
16
  load(): Promise<boolean>;
17
17
  run(name: string, context: PluginCommandContextV1): Promise<PluginCommandResultV1 | null>;
18
+ runSessionStart(context: Parameters<PluginSessionStartHookV1["run"]>[0]): Promise<PluginSessionUsageDecisionV1 | null>;
18
19
  private createHost;
19
20
  private refreshEntitlement;
20
21
  }
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import * as fs from "node:fs";
2
3
  import * as path from "node:path";
3
4
  import { pathToFileURL } from "node:url";
@@ -102,6 +103,35 @@ export class InstalledPluginRuntimeV1 {
102
103
  };
103
104
  return registered.command.run(context);
104
105
  }
106
+ async runSessionStart(context) {
107
+ if (!(await this.load()) || this.hooks.length === 0)
108
+ return null;
109
+ const entitlement = await this.refreshEntitlement();
110
+ const eligible = this.hooks.filter((hook) => isPluginCapabilityEnabled(entitlement, hook.requiredCapability));
111
+ if (eligible.length === 0)
112
+ return null;
113
+ const metered = eligible.some((hook) => entitlement.capabilities[hook.requiredCapability]?.quota?.scope === "account");
114
+ if (!metered) {
115
+ for (const hook of eligible)
116
+ await hook.run(context);
117
+ return null;
118
+ }
119
+ if (!this.backend.reserveSession || !this.backend.commitSession || !this.backend.releaseSession)
120
+ throw new PluginBootstrapFailure("session_usage_unavailable", "Account session metering is unavailable");
121
+ const operationId = randomUUID();
122
+ const reservation = await this.backend.reserveSession(operationId);
123
+ if (!reservation.allowed)
124
+ return reservation;
125
+ try {
126
+ for (const hook of eligible)
127
+ await hook.run(context);
128
+ return await this.backend.commitSession(operationId);
129
+ }
130
+ catch (error) {
131
+ await this.backend.releaseSession(operationId);
132
+ throw error;
133
+ }
134
+ }
105
135
  createHost(manifest) {
106
136
  const descriptors = new Map(manifest.commands.map((command) => [command.name, command]));
107
137
  const stateRoot = path.join(this.store.root, "state");
@@ -1,4 +1,4 @@
1
- import { type PluginAccessDecisionV1, type PluginBootstrapBackendV1, type PluginNextActionV1, type SignedPluginReleaseV1 } from "./plugin-bootstrap.js";
1
+ import { type PluginAccessDecisionV1, type PluginBootstrapBackendV1, type PluginNextActionV1, type PluginSessionUsageDecisionV1, type SignedPluginReleaseV1 } from "./plugin-bootstrap.js";
2
2
  import { type PluginEntitlementStatusV1 } from "./plugin-host.js";
3
3
  interface TemporaryPluginBackendOptions {
4
4
  root?: string;
@@ -27,6 +27,9 @@ export declare class TemporaryPluginBackend implements PluginBootstrapBackendV1
27
27
  channel: string;
28
28
  allowAuthentication: boolean;
29
29
  }): Promise<PluginAccessDecisionV1>;
30
+ reserveSession(operationId: string): Promise<PluginSessionUsageDecisionV1>;
31
+ commitSession(operationId: string): Promise<PluginSessionUsageDecisionV1>;
32
+ releaseSession(operationId: string): Promise<PluginSessionUsageDecisionV1>;
30
33
  listReleases(request: {
31
34
  bundleId: string;
32
35
  channel: string;
@@ -40,6 +43,7 @@ export declare class TemporaryPluginBackend implements PluginBootstrapBackendV1
40
43
  private activationPath;
41
44
  private readActivation;
42
45
  private writeActivation;
46
+ private sessionUsage;
43
47
  private request;
44
48
  }
45
49
  export {};
@@ -12,31 +12,37 @@ const REQUEST_TIMEOUT_MS = 30_000;
12
12
  const EMAIL_MAX_BYTES = 254;
13
13
  const FORM_MAX_BYTES = 2_048;
14
14
  const SERVICE_JSON_MAX_BYTES = 1024 * 1024;
15
+ const ACTIVATION_CREDENTIAL = /^am_activation_[A-Za-z0-9_-]{32,256}$/;
15
16
  const MISSING_ENTITLEMENT = {
16
17
  plan: null,
17
18
  state: "missing",
18
19
  features: [],
19
20
  capabilities: {},
20
- reason: "Enter an email address to activate temporary unlimited local use",
21
- };
22
- const TEMPORARY_ENTITLEMENT = {
23
- plan: "pro",
24
- state: "active",
25
- features: ["session-intelligence", "web-console"],
26
- capabilities: Object.fromEntries([
27
- "session-index",
28
- "session-worker",
29
- "learning",
30
- "retrieval-evaluation",
31
- "operational-metrics",
32
- "web-console",
33
- "memory-explorer",
34
- ].map((capability) => [capability, { enabled: true }])),
35
- reason: "Temporary email activation grants unlimited local use",
21
+ reason: "Enter an email address to activate the free daily session allowance",
36
22
  };
37
23
  function cloneEntitlement(value) {
38
24
  return structuredClone(value);
39
25
  }
26
+ function freeEntitlement(dailySessionLimit) {
27
+ return {
28
+ plan: "free",
29
+ state: "active",
30
+ features: ["session-intelligence", "web-console"],
31
+ capabilities: {
32
+ "session-index": { enabled: true },
33
+ "session-worker": {
34
+ enabled: true,
35
+ quota: { limit: dailySessionLimit, window: "day", scope: "account" },
36
+ },
37
+ learning: { enabled: true },
38
+ "retrieval-evaluation": { enabled: true },
39
+ "operational-metrics": { enabled: true },
40
+ "web-console": { enabled: true },
41
+ "memory-explorer": { enabled: true },
42
+ },
43
+ reason: `${dailySessionLimit} free agent sessions per UTC day`,
44
+ };
45
+ }
40
46
  function isEmail(value) {
41
47
  return (Buffer.byteLength(value, "utf-8") <= EMAIL_MAX_BYTES &&
42
48
  [...value].every((character) => character.charCodeAt(0) >= 32 && character.charCodeAt(0) !== 127) &&
@@ -53,7 +59,7 @@ function securityHeaders(contentType) {
53
59
  }
54
60
  function activationPage(action, error) {
55
61
  const errorHtml = error ? `<p class="error">${error}</p>` : "";
56
- return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Activate AgentMemory Pro</title><style>body{font:16px system-ui;max-width:34rem;margin:10vh auto;padding:0 1.5rem;color:#18212b}form{display:grid;gap:1rem}input,button{font:inherit;padding:.8rem;border-radius:.5rem;border:1px solid #aab4bf}button{background:#18212b;color:#fff;cursor:pointer}.muted{color:#586574}.error{color:#a21d24}</style></head><body><h1>Activate AgentMemory Pro</h1><p>Enter an email address to enable temporary unlimited use on this device.</p>${errorHtml}<form method="post" action="${action}"><label>Email <input type="email" name="email" autocomplete="email" maxlength="254" required autofocus></label><button type="submit">Activate and return to terminal</button></form><p class="muted">The AgentMemory CLI sends your email plus core, bundle, platform, architecture, and release-channel metadata to the private activation service. The activation record never includes memory, session content, queries, repository paths, IP addresses, or user-agent strings, and expires after 365 days without activation.</p></body></html>`;
62
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Activate AgentMemory</title><style>body{font:16px system-ui;max-width:34rem;margin:10vh auto;padding:0 1.5rem;color:#18212b}form{display:grid;gap:1rem}input,button{font:inherit;padding:.8rem;border-radius:.5rem;border:1px solid #aab4bf}button{background:#18212b;color:#fff;cursor:pointer}.muted{color:#586574}.error{color:#a21d24}</style></head><body><h1>Activate AgentMemory</h1><p>Enter an email address to enable the free daily agent-session allowance on this device.</p>${errorHtml}<form method="post" action="${action}"><label>Email <input type="email" name="email" autocomplete="email" maxlength="254" required autofocus></label><button type="submit">Activate and return to terminal</button></form><p class="muted">The AgentMemory CLI sends your email plus core, bundle, platform, architecture, and release-channel metadata to the private activation service. D1 stores a daily count of opaque SessionStart operations for your normalized email. The request never includes memory, session content, queries, repository paths, raw agent session identifiers, IP addresses, or user-agent strings. Activation records expire after 365 days without use.</p></body></html>`;
57
63
  }
58
64
  function completionPage() {
59
65
  return '<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>AgentMemory activated</title></head><body><h1>Activation complete</h1><p>You can close this tab and return to the terminal.</p></body></html>';
@@ -253,11 +259,13 @@ export class TemporaryPluginBackend {
253
259
  this.activate = options.activate ?? (() => collectTemporaryActivation(this.openUrl));
254
260
  }
255
261
  async getLocalEntitlement() {
256
- return this.readActivation() ? cloneEntitlement(TEMPORARY_ENTITLEMENT) : cloneEntitlement(MISSING_ENTITLEMENT);
262
+ const activation = this.readActivation();
263
+ return activation ? freeEntitlement(activation.dailySessionLimit) : cloneEntitlement(MISSING_ENTITLEMENT);
257
264
  }
258
265
  async resolveAccess(request) {
259
- let activation = this.readActivation();
260
- if (!activation) {
266
+ const activation = this.readActivation();
267
+ let email = activation?.email;
268
+ if (!email) {
261
269
  if (!request.allowAuthentication)
262
270
  return {
263
271
  kind: "auth_required",
@@ -268,33 +276,48 @@ export class TemporaryPluginBackend {
268
276
  message: "Run plugin install in an interactive terminal to enter an email address",
269
277
  },
270
278
  };
271
- const email = await this.activate();
272
- this.writeActivation(email);
273
- activation = this.readActivation();
279
+ email = await this.activate();
274
280
  }
275
- if (!activation)
276
- throw new PluginBootstrapFailure("activation_failed", "The local activation record could not be loaded");
277
281
  const response = await this.request(`${this.apiOrigin}/v1/plugin/access`, {
278
282
  method: "POST",
279
283
  headers: { "Content-Type": "application/json" },
280
284
  body: JSON.stringify({
281
285
  schemaVersion: 1,
282
- email: activation.email,
286
+ email,
283
287
  bundleId: request.bundleId,
284
288
  installedVersion: request.installedVersion ?? null,
285
289
  coreVersion: this.coreVersion,
286
290
  channel: request.channel,
287
291
  platform: process.platform,
288
292
  architecture: process.arch,
289
- consentVersion: "activation-v1",
293
+ consentVersion: "activation-v2",
290
294
  }),
291
295
  });
292
296
  const value = (await readJson(response));
293
297
  validatePluginEntitlementStatusV1(value.entitlement);
294
298
  if (typeof value.artifactGrant !== "string" || !value.artifactGrant)
295
299
  throw new PluginBootstrapFailure("service_response_invalid", "The access response omitted its artifact grant");
300
+ if (typeof value.usageCredential !== "string" || !ACTIVATION_CREDENTIAL.test(value.usageCredential))
301
+ throw new PluginBootstrapFailure("service_response_invalid", "The access response omitted its usage credential");
302
+ const freeQuota = value.entitlement.capabilities["session-worker"]?.quota;
303
+ if (value.entitlement.plan !== "free" ||
304
+ value.entitlement.state !== "active" ||
305
+ !freeQuota ||
306
+ freeQuota.scope !== "account" ||
307
+ freeQuota.window !== "day")
308
+ throw new PluginBootstrapFailure("service_response_invalid", "The free session policy is invalid");
309
+ this.writeActivation(email, value.usageCredential, freeQuota.limit);
296
310
  return { kind: "granted", entitlement: value.entitlement, artifactGrant: value.artifactGrant };
297
311
  }
312
+ async reserveSession(operationId) {
313
+ return this.sessionUsage("reserve", operationId);
314
+ }
315
+ async commitSession(operationId) {
316
+ return this.sessionUsage("commit", operationId);
317
+ }
318
+ async releaseSession(operationId) {
319
+ return this.sessionUsage("release", operationId);
320
+ }
298
321
  async listReleases(request) {
299
322
  const response = await this.request(`${this.apiOrigin}/v1/plugin/releases`, {
300
323
  headers: { Authorization: `Bearer ${request.artifactGrant}` },
@@ -338,17 +361,27 @@ export class TemporaryPluginBackend {
338
361
  if (!stat.isFile() || stat.isSymbolicLink() || (process.platform !== "win32" && (stat.mode & 0o077) !== 0))
339
362
  return null;
340
363
  const value = JSON.parse(fs.readFileSync(activationPath, "utf-8"));
341
- return value.schemaVersion === 1 && isEmail(value.email) && Number.isFinite(Date.parse(value.activatedAt))
342
- ? value
343
- : null;
364
+ if (value.schemaVersion !== 2 ||
365
+ !isEmail(value.email) ||
366
+ !Number.isFinite(Date.parse(value.activatedAt)) ||
367
+ !ACTIVATION_CREDENTIAL.test(value.usageCredential) ||
368
+ !Number.isSafeInteger(value.dailySessionLimit) ||
369
+ value.dailySessionLimit <= 0 ||
370
+ value.dailySessionLimit > 10_000)
371
+ return null;
372
+ return value;
344
373
  }
345
374
  catch {
346
375
  return null;
347
376
  }
348
377
  }
349
- writeActivation(email) {
378
+ writeActivation(email, usageCredential, dailySessionLimit) {
350
379
  if (!isEmail(email))
351
380
  throw new PluginBootstrapFailure("email_invalid", "Enter a valid email address");
381
+ if (!ACTIVATION_CREDENTIAL.test(usageCredential))
382
+ throw new PluginBootstrapFailure("activation_failed", "The activation credential is invalid");
383
+ if (!Number.isSafeInteger(dailySessionLimit) || dailySessionLimit <= 0 || dailySessionLimit > 10_000)
384
+ throw new PluginBootstrapFailure("activation_failed", "The free session allowance is invalid");
352
385
  const target = this.activationPath();
353
386
  fs.mkdirSync(this.root, { recursive: true, mode: 0o700 });
354
387
  const rootStat = fs.lstatSync(this.root);
@@ -361,9 +394,38 @@ export class TemporaryPluginBackend {
361
394
  if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink())
362
395
  throw new PluginBootstrapFailure("activation_path_invalid", "The plugin activation directory is unsafe");
363
396
  const temporary = `${target}.tmp-${process.pid}-${randomUUID()}`;
364
- fs.writeFileSync(temporary, `${JSON.stringify({ schemaVersion: 1, email, activatedAt: new Date().toISOString() }, null, 2)}\n`, { mode: 0o600, flag: "wx" });
397
+ fs.writeFileSync(temporary, `${JSON.stringify({ schemaVersion: 2, email, activatedAt: new Date().toISOString(), usageCredential, dailySessionLimit }, null, 2)}\n`, { mode: 0o600, flag: "wx" });
365
398
  fs.renameSync(temporary, target);
366
399
  }
400
+ async sessionUsage(action, operationId) {
401
+ const activation = this.readActivation();
402
+ if (!activation)
403
+ throw new PluginBootstrapFailure("auth_required", "Run plugin install to activate AgentMemory");
404
+ const response = await this.request(`${this.apiOrigin}/v1/plugin/sessions/${action}`, {
405
+ method: "POST",
406
+ headers: {
407
+ Authorization: `Bearer ${activation.usageCredential}`,
408
+ "Content-Type": "application/json",
409
+ },
410
+ body: JSON.stringify({ schemaVersion: 1, operationId }),
411
+ });
412
+ const value = (await readJson(response));
413
+ const decision = value.decision;
414
+ if (!decision ||
415
+ typeof decision.allowed !== "boolean" ||
416
+ !["reserved", "committed", "released", "exhausted", "missing"].includes(String(decision.state)) ||
417
+ !Number.isSafeInteger(decision.limit) ||
418
+ Number(decision.limit) <= 0 ||
419
+ !Number.isSafeInteger(decision.used) ||
420
+ Number(decision.used) < 0 ||
421
+ !Number.isSafeInteger(decision.remaining) ||
422
+ Number(decision.remaining) < 0 ||
423
+ typeof decision.resetAt !== "string" ||
424
+ !Number.isFinite(Date.parse(decision.resetAt)) ||
425
+ typeof decision.idempotent !== "boolean")
426
+ throw new PluginBootstrapFailure("service_response_invalid", "The session usage response is invalid");
427
+ return decision;
428
+ }
367
429
  async request(url, init = {}) {
368
430
  let response;
369
431
  try {
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Status
4
4
 
5
- Accepted design on 2026-08-16. The public core now implements host types, temporary loopback activation, live catalog and artifact retrieval, Ed25519 release verification, bounded package validation, transactional install, bundle health checks, and paid-command dispatch. The temporary beta grants unlimited local use after email entry; durable authentication, payment, renewal, and account management remain deferred.
5
+ Accepted design on 2026-08-16 and revised on 2026-08-17. The public core implements host types, loopback email activation, live catalog and artifact retrieval, Ed25519 release verification, bounded package validation, transactional install, bundle health checks, paid-command dispatch, and SessionStart hook dispatch. The free plan grants a configurable number of agent sessions per normalized email and UTC day; durable account authentication, payment, renewal, and account management remain deferred.
6
6
 
7
7
  The public `agentmemory` repository and `myagentmemory` npm package remain the free, MIT-licensed core. The public bootstrap client and host contracts are also MIT-licensed. Official commercial implementations and browser assets are built and distributed separately from the private `agent-memory-plugin` workspace under their own terms. Pricing, the billing provider, device limits, offline-grace duration, and Enterprise contract terms are intentionally not decided here. The temporary beta currently uses allowlisted `*.agentmemory.paperpilot.me` service origins; changing those origins is a public-client release change.
8
8
 
@@ -23,11 +23,11 @@ agent-memory plugin install
23
23
  | Plugin absent | Active or grace | Install the compatible signed bundle |
24
24
  | Plugin older than the selected release | Active or grace | Upgrade atomically |
25
25
  | Plugin current | Active or grace | Report that it is current |
26
- | Any | Missing | Start temporary loopback email activation in an interactive terminal |
26
+ | Any | Missing | Start loopback email activation in an interactive terminal |
27
27
  | Any | Expired | Direct the user to renewal; leave core available |
28
28
  | Incompatible bundle | Any | Leave the current version untouched and explain the required core version |
29
29
 
30
- An absent plugin cannot activate itself. The public bootstrap performs temporary local activation, obtains a short-lived artifact grant, and verifies the release and artifact. After installation, the public host validates the local activation record and checks the required capability before every paid command. Signed long-lived entitlements will replace this temporary record when authentication and payment ship.
30
+ An absent plugin cannot activate itself. The public bootstrap collects an email locally, obtains a server-issued usage credential and short-lived artifact grant, and verifies the release and artifact. The credential is persisted only after the service accepts activation. After installation, the public host reconstructs the free account-metered capability policy in core code and checks required capabilities before commands and hooks. Signed long-lived entitlements can extend this credential when authentication and payment ship.
31
31
 
32
32
  ## Ownership boundary
33
33
 
@@ -81,7 +81,7 @@ agent-memory plugin manage [--no-browser]
81
81
  - `status` is read-only. It reports the installed bundle, selected channel, compatibility, entitlement state, and update availability.
82
82
  - `install` authenticates when necessary, then installs, upgrades, or reports current state.
83
83
  - `update` requires an existing installation and never starts a new purchase implicitly.
84
- - `uninstall` removes executable plugin material and the active receipt. It preserves core memory, plugin state, and the temporary activation record.
84
+ - `uninstall` removes executable plugin material and the active receipt. It preserves core memory, plugin state, and the permission-restricted activation credential.
85
85
  - `manage` remains unavailable until authenticated account and billing management exists.
86
86
 
87
87
  Installed plugins contribute top-level commands such as `recall`, `learn`, `worker`, and `web`. Bootstrap command names are reserved by the core and cannot be replaced by a plugin.
@@ -133,16 +133,17 @@ Every bootstrap command supports `--json` and emits one JSON document with a ver
133
133
 
134
134
  `result` is one of `not_installed`, `installed`, `upgraded`, `current`, `update_available`, `uninstalled`, `auth_required`, `renewal_required`, or `unavailable`. Failures use `ok: false` plus a stable `error.code` and redacted `error.message`. Output must never contain access tokens, download credentials, signed entitlement contents, local memory paths, or URLs containing bearer credentials.
135
135
 
136
- ## Temporary activation flow
136
+ ## Free activation flow
137
137
 
138
138
  1. `agent-memory plugin install` starts an HTTP server bound to `127.0.0.1` on an ephemeral port.
139
139
  2. The CLI prints and opens a nonce-bearing local URL. The page accepts one email address with bounded input, an exact Host and nonce path, same-origin browser request validation, restrictive response headers, and a five-minute deadline.
140
- 3. Submission writes a mode-0600 record under the plugin install root and returns a completion page.
141
- 4. The waiting CLI sends the email plus core, installed-bundle, platform, architecture, release-channel, and consent-version fields to the private control plane. Its activation database stores none of the user's memory, sessions, queries, repository paths, IP address, or user-agent string, and deletes records after 365 days without activation.
142
- 5. The CLI requests temporary unlimited capabilities and a short-lived object-bound artifact grant, verifies the Ed25519-signed release plus package digest and limits, imports it for health checks, then atomically activates the receipt.
143
- 6. Installed paid commands reload the local entitlement and enforce their declared capability before execution.
140
+ 3. Submission returns a completion page but does not create a local credential yet.
141
+ 4. The waiting CLI sends the email plus core, installed-bundle, platform, architecture, release-channel, and consent-version fields to the private control plane. Its activation database stores none of the user's memory, session content, queries, repository paths, raw agent session identifiers, IP address, or user-agent string.
142
+ 5. The service normalizes the email, stores only a hash of a random usage credential, and returns the credential, a free account-metered entitlement, and a short-lived object-bound artifact grant. Only then does the CLI atomically write a mode-0600 activation record.
143
+ 6. The CLI verifies the Ed25519-signed release plus package digest and limits, imports it for health checks, and atomically activates the receipt.
144
+ 7. Each paid SessionStart hook reserves one opaque operation against the email's UTC-day allowance, commits after useful hook work, and releases on failure. Exhaustion skips paid hook work without affecting public-core context.
144
145
 
145
- This is explicitly temporary. Authentication, payment, renewal, account management, server-side entitlement state, and durable credential storage are not implemented yet.
146
+ Email ownership is not verified in this free flow. Authentication, payment, renewal, account management, and signed paid entitlements are not implemented yet.
146
147
 
147
148
  ## Future authentication and purchase flow
148
149
 
@@ -160,13 +161,14 @@ An Enterprise administrator may pre-provision an organization entitlement or man
160
161
 
161
162
  ## Control-plane boundary
162
163
 
163
- The temporary service exposes:
164
+ The service exposes:
164
165
 
165
- - `POST /v1/plugin/access` for unlimited temporary capability grants plus a short-lived artifact grant;
166
+ - `POST /v1/plugin/access` for a free account-metered entitlement, a usage credential, and a short-lived artifact grant;
167
+ - `POST /v1/plugin/sessions/reserve|commit|release` for atomic daily allowance enforcement;
166
168
  - `GET /v1/plugin/releases` for an Ed25519-signed release selected from the private R2 catalog;
167
169
  - `GET|HEAD /v1/artifacts/download` for the exact content-addressed object authorized by the bearer grant.
168
170
 
169
- The temporary access request contains the submitted email plus the bounded core, bundle, platform, architecture, release-channel, and consent-version fields described above. Its application payload contains no memory content, search query, session content, path, repository name, qmd data, IP address, user-agent string, or plugin-derived metric; the activation database stores neither IP addresses nor user-agent strings. Future authenticated service responsibilities include:
171
+ The access request contains the submitted email plus the bounded core, bundle, platform, architecture, release-channel, and consent-version fields described above. Session metering sends only a random operation ID and bearer credential; D1 associates those values with a normalized email and UTC-day counter. Application payloads contain no memory content, search query, session content, path, repository name, raw agent session identifier, qmd data, IP address, or user-agent string. The activation database stores neither IP addresses nor user-agent strings. Future authenticated service responsibilities include:
170
172
 
171
173
  - create and poll a device authorization;
172
174
  - read the authenticated principal's effective entitlement;
@@ -181,9 +183,9 @@ The bootstrap may send only:
181
183
  - core version, plugin-host API version, platform, and architecture;
182
184
  - requested bundle ID, installed bundle version, and release channel;
183
185
  - a pseudonymous license or organization identifier;
184
- - protocol nonces and authentication material required for the request.
186
+ - protocol nonces, opaque quota operation IDs, and authentication material required for the request.
185
187
 
186
- It must never send memory contents, search queries, session contents, working-directory names, repository names, filesystem paths, qmd data, or plugin-derived metrics. Product telemetry is not part of this protocol.
188
+ It must never send memory contents, search queries, session contents, raw agent session identifiers, working-directory names, repository names, filesystem paths, or qmd data. The bounded allowance counter is authorization state, not general product telemetry.
187
189
 
188
190
  Production builds use an allowlisted HTTPS origin. Development endpoint overrides must be explicit, must not silently affect production builds, and must never weaken TLS verification.
189
191
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myagentmemory",
3
- "version": "0.4.14",
3
+ "version": "0.4.15",
4
4
  "description": "agentmemory (agent-memory) is persistent memory for coding agents (Claude Code, OpenAI Codex, Cursor, Agent) with qmd-powered semantic search across daily logs, long-term memory, and scratchpad",
5
5
  "main": "./dist/core.js",
6
6
  "types": "./dist/core.d.ts",
package/src/cli.ts CHANGED
@@ -244,7 +244,9 @@ function printPluginResult(result: PluginBootstrapResultV1, json: boolean, allow
244
244
  console.log("Run: agent-memory plugin install");
245
245
  break;
246
246
  case "auth_required":
247
- console.log("Run this command in an interactive terminal to enter an email and activate temporary access.");
247
+ console.log(
248
+ "Run this command in an interactive terminal to enter an email and activate free daily access.",
249
+ );
248
250
  break;
249
251
  case "renewal_required":
250
252
  console.log("Renew AgentMemory Pro to continue using paid capabilities.");
@@ -1000,8 +1002,8 @@ Usage:
1000
1002
  agent-memory plugin manage [--no-browser]
1001
1003
 
1002
1004
  The public core remains fully usable without AgentMemory Pro. Interactive install
1003
- opens a loopback website for temporary email activation and unlimited local use.
1004
- Authentication and payment will be added later.`);
1005
+ opens a loopback website for email activation and a configurable free daily
1006
+ agent-session allowance. Memory and session content stay on this device.`);
1005
1007
  }
1006
1008
 
1007
1009
  function pluginCommandFailure(command: string, error: unknown): PluginBootstrapResultV1 {
@@ -1227,6 +1229,17 @@ async function main() {
1227
1229
  const agent = getFlag(flags, "agent");
1228
1230
  if (!agent) exitError("hook session-start requires --agent", json);
1229
1231
  await cmdContext({ "no-search": true });
1232
+ try {
1233
+ const decision = await new InstalledPluginRuntimeV1({ coreVersion: VERSION }).runSessionStart({
1234
+ host: agent,
1235
+ cwd: process.cwd(),
1236
+ signal: new AbortController().signal,
1237
+ });
1238
+ if (decision?.state === "exhausted")
1239
+ console.error(`AgentMemory free session allowance resets at ${decision.resetAt}`);
1240
+ } catch {
1241
+ // Paid SessionStart work must never make public-core context unavailable.
1242
+ }
1230
1243
  break;
1231
1244
  }
1232
1245
  case "plugin":
@@ -39,6 +39,16 @@ export interface PluginNextActionV1 {
39
39
  message?: string;
40
40
  }
41
41
 
42
+ export interface PluginSessionUsageDecisionV1 {
43
+ allowed: boolean;
44
+ state: "reserved" | "committed" | "released" | "exhausted" | "missing";
45
+ limit: number;
46
+ used: number;
47
+ remaining: number;
48
+ resetAt: string;
49
+ idempotent: boolean;
50
+ }
51
+
42
52
  export interface PluginInstallReceiptV1 {
43
53
  schemaVersion: 1;
44
54
  bundleId: string;
@@ -139,6 +149,9 @@ export interface PluginBootstrapBackendV1 {
139
149
  artifactGrant: string;
140
150
  }): Promise<SignedPluginReleaseV1[]>;
141
151
  downloadArtifact(request: { release: SignedPluginReleaseV1; artifactGrant: string }): Promise<Uint8Array>;
152
+ reserveSession?(operationId: string): Promise<PluginSessionUsageDecisionV1>;
153
+ commitSession?(operationId: string): Promise<PluginSessionUsageDecisionV1>;
154
+ releaseSession?(operationId: string): Promise<PluginSessionUsageDecisionV1>;
142
155
  getManagementAction(): Promise<PluginNextActionV1 | null>;
143
156
  }
144
157
 
@@ -17,7 +17,7 @@ export type PluginPlanV1 = "free" | "trial" | "pro" | "team" | "enterprise";
17
17
  export interface PluginCapabilityQuotaV1 {
18
18
  limit: number;
19
19
  window: "day";
20
- scope: "device";
20
+ scope: "device" | "account";
21
21
  }
22
22
 
23
23
  export interface PluginCapabilityGrantV1 {
@@ -218,7 +218,7 @@ export function validatePluginEntitlementStatusV1(
218
218
  if (grant.quota) {
219
219
  if (!Number.isInteger(grant.quota.limit) || grant.quota.limit <= 0)
220
220
  throw new Error(`Capability ${capability} has an invalid quota limit`);
221
- if (grant.quota.window !== "day" || grant.quota.scope !== "device")
221
+ if (grant.quota.window !== "day" || !["device", "account"].includes(grant.quota.scope))
222
222
  throw new Error(`Capability ${capability} has an invalid quota policy`);
223
223
  }
224
224
  }
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import * as fs from "node:fs";
2
3
  import * as path from "node:path";
3
4
  import { pathToFileURL } from "node:url";
@@ -10,6 +11,7 @@ import {
10
11
  PluginBootstrapFailure,
11
12
  type PluginInstallReceiptV1,
12
13
  type PluginInstallStoreV1,
14
+ type PluginSessionUsageDecisionV1,
13
15
  type SignedPluginReleaseV1,
14
16
  } from "./plugin-bootstrap.js";
15
17
  import {
@@ -172,6 +174,34 @@ export class InstalledPluginRuntimeV1 {
172
174
  return registered.command.run(context);
173
175
  }
174
176
 
177
+ async runSessionStart(
178
+ context: Parameters<PluginSessionStartHookV1["run"]>[0],
179
+ ): Promise<PluginSessionUsageDecisionV1 | null> {
180
+ if (!(await this.load()) || this.hooks.length === 0) return null;
181
+ const entitlement = await this.refreshEntitlement();
182
+ const eligible = this.hooks.filter((hook) => isPluginCapabilityEnabled(entitlement, hook.requiredCapability));
183
+ if (eligible.length === 0) return null;
184
+ const metered = eligible.some(
185
+ (hook) => entitlement.capabilities[hook.requiredCapability]?.quota?.scope === "account",
186
+ );
187
+ if (!metered) {
188
+ for (const hook of eligible) await hook.run(context);
189
+ return null;
190
+ }
191
+ if (!this.backend.reserveSession || !this.backend.commitSession || !this.backend.releaseSession)
192
+ throw new PluginBootstrapFailure("session_usage_unavailable", "Account session metering is unavailable");
193
+ const operationId = randomUUID();
194
+ const reservation = await this.backend.reserveSession(operationId);
195
+ if (!reservation.allowed) return reservation;
196
+ try {
197
+ for (const hook of eligible) await hook.run(context);
198
+ return await this.backend.commitSession(operationId);
199
+ } catch (error) {
200
+ await this.backend.releaseSession(operationId);
201
+ throw error;
202
+ }
203
+ }
204
+
175
205
  private createHost(manifest: AgentMemoryPluginManifestV1): AgentMemoryPluginHostV1 {
176
206
  const descriptors = new Map(manifest.commands.map((command) => [command.name, command]));
177
207
  const stateRoot = path.join(this.store.root, "state");
@@ -10,6 +10,7 @@ import {
10
10
  type PluginBootstrapBackendV1,
11
11
  PluginBootstrapFailure,
12
12
  type PluginNextActionV1,
13
+ type PluginSessionUsageDecisionV1,
13
14
  type SignedPluginReleaseV1,
14
15
  } from "./plugin-bootstrap.js";
15
16
  import { type PluginEntitlementStatusV1, validatePluginEntitlementStatusV1 } from "./plugin-host.js";
@@ -21,37 +22,22 @@ const REQUEST_TIMEOUT_MS = 30_000;
21
22
  const EMAIL_MAX_BYTES = 254;
22
23
  const FORM_MAX_BYTES = 2_048;
23
24
  const SERVICE_JSON_MAX_BYTES = 1024 * 1024;
25
+ const ACTIVATION_CREDENTIAL = /^am_activation_[A-Za-z0-9_-]{32,256}$/;
24
26
 
25
27
  const MISSING_ENTITLEMENT: PluginEntitlementStatusV1 = {
26
28
  plan: null,
27
29
  state: "missing",
28
30
  features: [],
29
31
  capabilities: {},
30
- reason: "Enter an email address to activate temporary unlimited local use",
31
- };
32
-
33
- const TEMPORARY_ENTITLEMENT: PluginEntitlementStatusV1 = {
34
- plan: "pro",
35
- state: "active",
36
- features: ["session-intelligence", "web-console"],
37
- capabilities: Object.fromEntries(
38
- [
39
- "session-index",
40
- "session-worker",
41
- "learning",
42
- "retrieval-evaluation",
43
- "operational-metrics",
44
- "web-console",
45
- "memory-explorer",
46
- ].map((capability) => [capability, { enabled: true }]),
47
- ),
48
- reason: "Temporary email activation grants unlimited local use",
32
+ reason: "Enter an email address to activate the free daily session allowance",
49
33
  };
50
34
 
51
35
  interface TemporaryActivationV1 {
52
- schemaVersion: 1;
36
+ schemaVersion: 2;
53
37
  email: string;
54
38
  activatedAt: string;
39
+ usageCredential: string;
40
+ dailySessionLimit: number;
55
41
  }
56
42
 
57
43
  interface TemporaryPluginBackendOptions {
@@ -68,6 +54,27 @@ function cloneEntitlement(value: PluginEntitlementStatusV1): PluginEntitlementSt
68
54
  return structuredClone(value);
69
55
  }
70
56
 
57
+ function freeEntitlement(dailySessionLimit: number): PluginEntitlementStatusV1 {
58
+ return {
59
+ plan: "free",
60
+ state: "active",
61
+ features: ["session-intelligence", "web-console"],
62
+ capabilities: {
63
+ "session-index": { enabled: true },
64
+ "session-worker": {
65
+ enabled: true,
66
+ quota: { limit: dailySessionLimit, window: "day", scope: "account" },
67
+ },
68
+ learning: { enabled: true },
69
+ "retrieval-evaluation": { enabled: true },
70
+ "operational-metrics": { enabled: true },
71
+ "web-console": { enabled: true },
72
+ "memory-explorer": { enabled: true },
73
+ },
74
+ reason: `${dailySessionLimit} free agent sessions per UTC day`,
75
+ };
76
+ }
77
+
71
78
  function isEmail(value: string): boolean {
72
79
  return (
73
80
  Buffer.byteLength(value, "utf-8") <= EMAIL_MAX_BYTES &&
@@ -89,7 +96,7 @@ function securityHeaders(contentType: string): Record<string, string> {
89
96
 
90
97
  function activationPage(action: string, error?: string): string {
91
98
  const errorHtml = error ? `<p class="error">${error}</p>` : "";
92
- return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Activate AgentMemory Pro</title><style>body{font:16px system-ui;max-width:34rem;margin:10vh auto;padding:0 1.5rem;color:#18212b}form{display:grid;gap:1rem}input,button{font:inherit;padding:.8rem;border-radius:.5rem;border:1px solid #aab4bf}button{background:#18212b;color:#fff;cursor:pointer}.muted{color:#586574}.error{color:#a21d24}</style></head><body><h1>Activate AgentMemory Pro</h1><p>Enter an email address to enable temporary unlimited use on this device.</p>${errorHtml}<form method="post" action="${action}"><label>Email <input type="email" name="email" autocomplete="email" maxlength="254" required autofocus></label><button type="submit">Activate and return to terminal</button></form><p class="muted">The AgentMemory CLI sends your email plus core, bundle, platform, architecture, and release-channel metadata to the private activation service. The activation record never includes memory, session content, queries, repository paths, IP addresses, or user-agent strings, and expires after 365 days without activation.</p></body></html>`;
99
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Activate AgentMemory</title><style>body{font:16px system-ui;max-width:34rem;margin:10vh auto;padding:0 1.5rem;color:#18212b}form{display:grid;gap:1rem}input,button{font:inherit;padding:.8rem;border-radius:.5rem;border:1px solid #aab4bf}button{background:#18212b;color:#fff;cursor:pointer}.muted{color:#586574}.error{color:#a21d24}</style></head><body><h1>Activate AgentMemory</h1><p>Enter an email address to enable the free daily agent-session allowance on this device.</p>${errorHtml}<form method="post" action="${action}"><label>Email <input type="email" name="email" autocomplete="email" maxlength="254" required autofocus></label><button type="submit">Activate and return to terminal</button></form><p class="muted">The AgentMemory CLI sends your email plus core, bundle, platform, architecture, and release-channel metadata to the private activation service. D1 stores a daily count of opaque SessionStart operations for your normalized email. The request never includes memory, session content, queries, repository paths, raw agent session identifiers, IP addresses, or user-agent strings. Activation records expire after 365 days without use.</p></body></html>`;
93
100
  }
94
101
 
95
102
  function completionPage(): string {
@@ -294,7 +301,8 @@ export class TemporaryPluginBackend implements PluginBootstrapBackendV1 {
294
301
  }
295
302
 
296
303
  async getLocalEntitlement(): Promise<PluginEntitlementStatusV1> {
297
- return this.readActivation() ? cloneEntitlement(TEMPORARY_ENTITLEMENT) : cloneEntitlement(MISSING_ENTITLEMENT);
304
+ const activation = this.readActivation();
305
+ return activation ? freeEntitlement(activation.dailySessionLimit) : cloneEntitlement(MISSING_ENTITLEMENT);
298
306
  }
299
307
 
300
308
  async resolveAccess(request: {
@@ -303,8 +311,9 @@ export class TemporaryPluginBackend implements PluginBootstrapBackendV1 {
303
311
  channel: string;
304
312
  allowAuthentication: boolean;
305
313
  }): Promise<PluginAccessDecisionV1> {
306
- let activation = this.readActivation();
307
- if (!activation) {
314
+ const activation = this.readActivation();
315
+ let email = activation?.email;
316
+ if (!email) {
308
317
  if (!request.allowAuthentication)
309
318
  return {
310
319
  kind: "auth_required",
@@ -315,34 +324,61 @@ export class TemporaryPluginBackend implements PluginBootstrapBackendV1 {
315
324
  message: "Run plugin install in an interactive terminal to enter an email address",
316
325
  },
317
326
  };
318
- const email = await this.activate();
319
- this.writeActivation(email);
320
- activation = this.readActivation();
327
+ email = await this.activate();
321
328
  }
322
- if (!activation)
323
- throw new PluginBootstrapFailure("activation_failed", "The local activation record could not be loaded");
324
329
  const response = await this.request(`${this.apiOrigin}/v1/plugin/access`, {
325
330
  method: "POST",
326
331
  headers: { "Content-Type": "application/json" },
327
332
  body: JSON.stringify({
328
333
  schemaVersion: 1,
329
- email: activation.email,
334
+ email,
330
335
  bundleId: request.bundleId,
331
336
  installedVersion: request.installedVersion ?? null,
332
337
  coreVersion: this.coreVersion,
333
338
  channel: request.channel,
334
339
  platform: process.platform,
335
340
  architecture: process.arch,
336
- consentVersion: "activation-v1",
341
+ consentVersion: "activation-v2",
337
342
  }),
338
343
  });
339
- const value = (await readJson(response)) as { entitlement?: unknown; artifactGrant?: unknown };
344
+ const value = (await readJson(response)) as {
345
+ entitlement?: unknown;
346
+ artifactGrant?: unknown;
347
+ usageCredential?: unknown;
348
+ };
340
349
  validatePluginEntitlementStatusV1(value.entitlement);
341
350
  if (typeof value.artifactGrant !== "string" || !value.artifactGrant)
342
351
  throw new PluginBootstrapFailure("service_response_invalid", "The access response omitted its artifact grant");
352
+ if (typeof value.usageCredential !== "string" || !ACTIVATION_CREDENTIAL.test(value.usageCredential))
353
+ throw new PluginBootstrapFailure(
354
+ "service_response_invalid",
355
+ "The access response omitted its usage credential",
356
+ );
357
+ const freeQuota = value.entitlement.capabilities["session-worker"]?.quota;
358
+ if (
359
+ value.entitlement.plan !== "free" ||
360
+ value.entitlement.state !== "active" ||
361
+ !freeQuota ||
362
+ freeQuota.scope !== "account" ||
363
+ freeQuota.window !== "day"
364
+ )
365
+ throw new PluginBootstrapFailure("service_response_invalid", "The free session policy is invalid");
366
+ this.writeActivation(email, value.usageCredential, freeQuota.limit);
343
367
  return { kind: "granted", entitlement: value.entitlement, artifactGrant: value.artifactGrant };
344
368
  }
345
369
 
370
+ async reserveSession(operationId: string): Promise<PluginSessionUsageDecisionV1> {
371
+ return this.sessionUsage("reserve", operationId);
372
+ }
373
+
374
+ async commitSession(operationId: string): Promise<PluginSessionUsageDecisionV1> {
375
+ return this.sessionUsage("commit", operationId);
376
+ }
377
+
378
+ async releaseSession(operationId: string): Promise<PluginSessionUsageDecisionV1> {
379
+ return this.sessionUsage("release", operationId);
380
+ }
381
+
346
382
  async listReleases(request: {
347
383
  bundleId: string;
348
384
  channel: string;
@@ -395,16 +431,28 @@ export class TemporaryPluginBackend implements PluginBootstrapBackendV1 {
395
431
  if (!stat.isFile() || stat.isSymbolicLink() || (process.platform !== "win32" && (stat.mode & 0o077) !== 0))
396
432
  return null;
397
433
  const value = JSON.parse(fs.readFileSync(activationPath, "utf-8")) as TemporaryActivationV1;
398
- return value.schemaVersion === 1 && isEmail(value.email) && Number.isFinite(Date.parse(value.activatedAt))
399
- ? value
400
- : null;
434
+ if (
435
+ value.schemaVersion !== 2 ||
436
+ !isEmail(value.email) ||
437
+ !Number.isFinite(Date.parse(value.activatedAt)) ||
438
+ !ACTIVATION_CREDENTIAL.test(value.usageCredential) ||
439
+ !Number.isSafeInteger(value.dailySessionLimit) ||
440
+ value.dailySessionLimit <= 0 ||
441
+ value.dailySessionLimit > 10_000
442
+ )
443
+ return null;
444
+ return value;
401
445
  } catch {
402
446
  return null;
403
447
  }
404
448
  }
405
449
 
406
- private writeActivation(email: string): void {
450
+ private writeActivation(email: string, usageCredential: string, dailySessionLimit: number): void {
407
451
  if (!isEmail(email)) throw new PluginBootstrapFailure("email_invalid", "Enter a valid email address");
452
+ if (!ACTIVATION_CREDENTIAL.test(usageCredential))
453
+ throw new PluginBootstrapFailure("activation_failed", "The activation credential is invalid");
454
+ if (!Number.isSafeInteger(dailySessionLimit) || dailySessionLimit <= 0 || dailySessionLimit > 10_000)
455
+ throw new PluginBootstrapFailure("activation_failed", "The free session allowance is invalid");
408
456
  const target = this.activationPath();
409
457
  fs.mkdirSync(this.root, { recursive: true, mode: 0o700 });
410
458
  const rootStat = fs.lstatSync(this.root);
@@ -418,12 +466,50 @@ export class TemporaryPluginBackend implements PluginBootstrapBackendV1 {
418
466
  const temporary = `${target}.tmp-${process.pid}-${randomUUID()}`;
419
467
  fs.writeFileSync(
420
468
  temporary,
421
- `${JSON.stringify({ schemaVersion: 1, email, activatedAt: new Date().toISOString() }, null, 2)}\n`,
469
+ `${JSON.stringify(
470
+ { schemaVersion: 2, email, activatedAt: new Date().toISOString(), usageCredential, dailySessionLimit },
471
+ null,
472
+ 2,
473
+ )}\n`,
422
474
  { mode: 0o600, flag: "wx" },
423
475
  );
424
476
  fs.renameSync(temporary, target);
425
477
  }
426
478
 
479
+ private async sessionUsage(
480
+ action: "reserve" | "commit" | "release",
481
+ operationId: string,
482
+ ): Promise<PluginSessionUsageDecisionV1> {
483
+ const activation = this.readActivation();
484
+ if (!activation) throw new PluginBootstrapFailure("auth_required", "Run plugin install to activate AgentMemory");
485
+ const response = await this.request(`${this.apiOrigin}/v1/plugin/sessions/${action}`, {
486
+ method: "POST",
487
+ headers: {
488
+ Authorization: `Bearer ${activation.usageCredential}`,
489
+ "Content-Type": "application/json",
490
+ },
491
+ body: JSON.stringify({ schemaVersion: 1, operationId }),
492
+ });
493
+ const value = (await readJson(response)) as { decision?: Partial<PluginSessionUsageDecisionV1> };
494
+ const decision = value.decision;
495
+ if (
496
+ !decision ||
497
+ typeof decision.allowed !== "boolean" ||
498
+ !["reserved", "committed", "released", "exhausted", "missing"].includes(String(decision.state)) ||
499
+ !Number.isSafeInteger(decision.limit) ||
500
+ Number(decision.limit) <= 0 ||
501
+ !Number.isSafeInteger(decision.used) ||
502
+ Number(decision.used) < 0 ||
503
+ !Number.isSafeInteger(decision.remaining) ||
504
+ Number(decision.remaining) < 0 ||
505
+ typeof decision.resetAt !== "string" ||
506
+ !Number.isFinite(Date.parse(decision.resetAt)) ||
507
+ typeof decision.idempotent !== "boolean"
508
+ )
509
+ throw new PluginBootstrapFailure("service_response_invalid", "The session usage response is invalid");
510
+ return decision as PluginSessionUsageDecisionV1;
511
+ }
512
+
427
513
  private async request(url: string, init: RequestInit = {}): Promise<Response> {
428
514
  let response: Response;
429
515
  try {