aismemory 0.7.4 → 0.8.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 (101) hide show
  1. package/README.md +211 -11
  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 +88 -7
  6. package/dist/__tests__/agent-key-mode.test.js.map +1 -1
  7. package/dist/__tests__/ais-http.test.js +32 -0
  8. package/dist/__tests__/ais-http.test.js.map +1 -1
  9. package/dist/__tests__/credential-isolation.test.js +117 -3
  10. package/dist/__tests__/credential-isolation.test.js.map +1 -1
  11. package/dist/__tests__/credential-resolution.test.js +5 -5
  12. package/dist/__tests__/credential-resolution.test.js.map +1 -1
  13. package/dist/__tests__/device-flow-gate-e2e.test.js +5 -0
  14. package/dist/__tests__/device-flow-gate-e2e.test.js.map +1 -1
  15. package/dist/__tests__/doctor.test.js +144 -79
  16. package/dist/__tests__/doctor.test.js.map +1 -1
  17. package/dist/__tests__/mcp-tools.test.d.ts +1 -0
  18. package/dist/__tests__/mcp-tools.test.js +189 -0
  19. package/dist/__tests__/mcp-tools.test.js.map +1 -0
  20. package/dist/__tests__/memory-pull.test.d.ts +1 -0
  21. package/dist/__tests__/memory-pull.test.js +380 -0
  22. package/dist/__tests__/memory-pull.test.js.map +1 -0
  23. package/dist/__tests__/memory-write-failure.test.js +4 -8
  24. package/dist/__tests__/memory-write-failure.test.js.map +1 -1
  25. package/dist/__tests__/no-registered-keys-disk-bearer-fallback.test.js +4 -7
  26. package/dist/__tests__/no-registered-keys-disk-bearer-fallback.test.js.map +1 -1
  27. package/dist/__tests__/pending-device-flow-e2e.test.d.ts +19 -0
  28. package/dist/__tests__/pending-device-flow-e2e.test.js +159 -0
  29. package/dist/__tests__/pending-device-flow-e2e.test.js.map +1 -0
  30. package/dist/__tests__/pending-device-flow.test.d.ts +15 -0
  31. package/dist/__tests__/pending-device-flow.test.js +108 -0
  32. package/dist/__tests__/pending-device-flow.test.js.map +1 -0
  33. package/dist/__tests__/provisional-register-mcp-remember.test.js +4 -8
  34. package/dist/__tests__/provisional-register-mcp-remember.test.js.map +1 -1
  35. package/dist/__tests__/readme-quickstart-default-ais-url.test.d.ts +1 -0
  36. package/dist/__tests__/readme-quickstart-default-ais-url.test.js +80 -0
  37. package/dist/__tests__/readme-quickstart-default-ais-url.test.js.map +1 -0
  38. package/dist/__tests__/reconnect-survival-e2e.test.js +3 -4
  39. package/dist/__tests__/reconnect-survival-e2e.test.js.map +1 -1
  40. package/dist/__tests__/remember-duplicate-truth.test.d.ts +13 -0
  41. package/dist/__tests__/remember-duplicate-truth.test.js +106 -0
  42. package/dist/__tests__/remember-duplicate-truth.test.js.map +1 -0
  43. package/dist/__tests__/retry-after.test.d.ts +4 -0
  44. package/dist/__tests__/retry-after.test.js +128 -0
  45. package/dist/__tests__/retry-after.test.js.map +1 -0
  46. package/dist/__tests__/startup-credential-warning-e2e.test.d.ts +1 -0
  47. package/dist/__tests__/startup-credential-warning-e2e.test.js +102 -0
  48. package/dist/__tests__/startup-credential-warning-e2e.test.js.map +1 -0
  49. package/dist/__tests__/tool-args.test.js +10 -0
  50. package/dist/__tests__/tool-args.test.js.map +1 -1
  51. package/dist/__tests__/version-check.test.d.ts +1 -0
  52. package/dist/__tests__/version-check.test.js +349 -0
  53. package/dist/__tests__/version-check.test.js.map +1 -0
  54. package/dist/agent-archive.d.ts +36 -0
  55. package/dist/agent-archive.js +69 -0
  56. package/dist/agent-archive.js.map +1 -0
  57. package/dist/agent-key-mode.d.ts +1 -1
  58. package/dist/agent-key-mode.js +56 -19
  59. package/dist/agent-key-mode.js.map +1 -1
  60. package/dist/ais-http-error.d.ts +26 -2
  61. package/dist/ais-http-error.js +52 -4
  62. package/dist/ais-http-error.js.map +1 -1
  63. package/dist/ais-http.js +46 -16
  64. package/dist/ais-http.js.map +1 -1
  65. package/dist/cli/doctor.d.ts +17 -2
  66. package/dist/cli/doctor.js +93 -14
  67. package/dist/cli/doctor.js.map +1 -1
  68. package/dist/cli/pull-memory.d.ts +17 -0
  69. package/dist/cli/pull-memory.js +141 -0
  70. package/dist/cli/pull-memory.js.map +1 -0
  71. package/dist/index.js +135 -398
  72. package/dist/index.js.map +1 -1
  73. package/dist/mcp/tools.d.ts +82 -0
  74. package/dist/mcp/tools.js +453 -0
  75. package/dist/mcp/tools.js.map +1 -0
  76. package/dist/memory-pull.d.ts +114 -0
  77. package/dist/memory-pull.js +171 -0
  78. package/dist/memory-pull.js.map +1 -0
  79. package/dist/pending-device-flow.d.ts +49 -0
  80. package/dist/pending-device-flow.js +96 -0
  81. package/dist/pending-device-flow.js.map +1 -0
  82. package/dist/retry-after.d.ts +26 -0
  83. package/dist/retry-after.js +42 -0
  84. package/dist/retry-after.js.map +1 -0
  85. package/dist/session-identity.d.ts +26 -0
  86. package/dist/session-identity.js +89 -0
  87. package/dist/session-identity.js.map +1 -0
  88. package/dist/telemetry.d.ts +1 -1
  89. package/dist/telemetry.js.map +1 -1
  90. package/dist/tool-args.d.ts +4 -0
  91. package/dist/tool-args.js +16 -0
  92. package/dist/tool-args.js.map +1 -1
  93. package/dist/version-check.d.ts +72 -0
  94. package/dist/version-check.js +237 -0
  95. package/dist/version-check.js.map +1 -0
  96. package/dist/workspace-key-mode.js +22 -6
  97. package/dist/workspace-key-mode.js.map +1 -1
  98. package/package.json +4 -3
  99. package/dist/harness-identity.d.ts +0 -17
  100. package/dist/harness-identity.js +0 -105
  101. package/dist/harness-identity.js.map +0 -1
package/dist/index.js CHANGED
@@ -9,15 +9,16 @@
9
9
  */
10
10
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
11
11
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
12
- import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
12
+ import { mkdirSync, writeFileSync } from 'node:fs';
13
13
  import { join } from 'node:path';
14
14
  import { tryKeyAuth, enrollKeyIfNeeded, hasEnrolledKey } from './key-auth.js';
15
- import { isCredsStale, isAuthErrorResult, isNoRegisteredKeysAuthError } from './auth-staleness.js';
15
+ import { isCredsStale, isNoRegisteredKeysAuthError } from './auth-staleness.js';
16
16
  import { AisHttpError } from './ais-http-error.js';
17
- import { aisGet, aisPost, aisPatch, DEFAULT_AIS_URL } from './ais-http.js';
17
+ import { aisGet, aisPost, DEFAULT_AIS_URL } from './ais-http.js';
18
18
  import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
19
19
  import { homedir } from 'node:os';
20
20
  import { loadOAuthCredentials, loadNonStaleDiskBearer, saveOAuthCredentials, } from './oauth-credentials.js';
21
+ import { clearPendingDeviceFlow, loadPendingDeviceFlow, savePendingDeviceFlow, } from './pending-device-flow.js';
21
22
  import { saveAgentPreference, loadAgentPreference, migrateCredentialsFileIfNeeded } from './agent-preference.js';
22
23
  import { createTokenManager, isNoRegisteredKeyFailure } from './token-manager.js';
23
24
  import { hydrateIdentity } from './hydration.js';
@@ -26,28 +27,18 @@ import { startRefreshTimer, stopRefreshTimer } from './refresh.js';
26
27
  import { ActivityTracker, setupAutoHandoff } from './auto-handoff.js';
27
28
  import { assertEnvAgentInTenant, selectAgentForSession } from './env-agent.js';
28
29
  import { emitTelemetry, hashAgentId } from './telemetry.js';
29
- import { parseAgentLoadArgs, parseEmptyToolArgs, parseHandoffArgs, parseRecallArgs, parseFullContextArgs, parseRememberArgs, parseInboxSendArgs, parseInboxReadArgs, parseInboxAckArgs, parseGoalCreateArgs, parseGoalListArgs, parseGoalUpdateProgressArgs, parseGoalCompleteArgs, INBOX_MAX_BODY_CHARS, INBOX_MAX_TASK_ID_CHARS, INBOX_MIN_TTL_SECONDS, INBOX_MAX_TTL_SECONDS, INBOX_DEFAULT_TTL_SECONDS, INBOX_MAX_PAGE_SIZE, INBOX_DEFAULT_PAGE_SIZE, INBOX_MAX_ACK_IDS, GOAL_MAX_TITLE_CHARS, GOAL_MAX_DESCRIPTION_CHARS, GOAL_MAX_CRITERIA_ITEMS, GOAL_DEFAULT_PAGE_SIZE, GOAL_MAX_PAGE_SIZE, } from './tool-args.js';
30
- import { loadHarnessIdentity, installHarnessIdentity, optionValue, assertPinnedAgent, verifyPinnedIdentity, requiresIdentityGate } from './harness-identity.js';
31
- import { readInboxAndMaybeAck } from './inbox-client.js';
30
+ import { INBOX_MAX_BODY_CHARS, INBOX_MAX_TASK_ID_CHARS, INBOX_MIN_TTL_SECONDS, INBOX_MAX_TTL_SECONDS, INBOX_DEFAULT_TTL_SECONDS, INBOX_MAX_PAGE_SIZE, INBOX_DEFAULT_PAGE_SIZE, INBOX_MAX_ACK_IDS, GOAL_MAX_TITLE_CHARS, GOAL_MAX_DESCRIPTION_CHARS, GOAL_MAX_CRITERIA_ITEMS, GOAL_DEFAULT_PAGE_SIZE, GOAL_MAX_PAGE_SIZE, } from './tool-args.js';
31
+ import { loadSessionIdentity, verifyPinnedIdentity } from './session-identity.js';
32
32
  import { runPollLoop, DeviceFlowRestartedError } from './device-poll.js';
33
- import { loadAgentApiKeyConfig, AgentApiKeyConfigError, dispatchToolKeyMode, UNKNOWN_TOOL_KEY_MODE, } from './agent-key-mode.js';
34
- import { loadWorkspaceKeyConfig, WorkspaceKeyConfigError, dispatchToolWorkspaceKeyMode, UNKNOWN_TOOL_WORKSPACE_KEY_MODE, } from './workspace-key-mode.js';
33
+ import { loadAgentApiKeyConfig, AgentApiKeyConfigError, } from './agent-key-mode.js';
34
+ import { loadWorkspaceKeyConfig, WorkspaceKeyConfigError, } from './workspace-key-mode.js';
35
+ import { createAgentKeyCallToolHandler, createOAuthCallToolHandler, createWorkspaceKeyCallToolHandler, } from './mcp/tools.js';
36
+ import { readPackageVersion, startVersionCheck } from './version-check.js';
37
+ import { AGENT_ARCHIVE_DESCRIPTION, AGENT_ARCHIVE_INPUT_SCHEMA, AGENT_DELETE_ALIAS_DESCRIPTION } from './agent-archive.js';
35
38
  // ── Config ──────────────────────────────────────────────
36
39
  const AIS_URL = process.env['AIS_URL']?.trim() || DEFAULT_AIS_URL;
37
40
  const PACKAGE_VERSION = readPackageVersion();
38
- let harnessIdentity = null;
39
- function readPackageVersion() {
40
- const packageJsonUrl = new URL('../package.json', import.meta.url);
41
- const packageJson = JSON.parse(readFileSync(packageJsonUrl, 'utf-8'));
42
- if (typeof packageJson !== 'object' ||
43
- packageJson === null ||
44
- !('version' in packageJson) ||
45
- typeof packageJson.version !== 'string' ||
46
- packageJson.version.length === 0) {
47
- throw new Error('Unable to load aismemory package version from package.json');
48
- }
49
- return packageJson.version;
50
- }
41
+ let sessionIdentity = null;
51
42
  // ── Credential persistence ──────────────────────────────
52
43
  function loadCredentials() {
53
44
  // F5a (CORBOT-48DD2E25): migrate a legacy v1 file to v2 (ids only) BEFORE
@@ -141,8 +132,8 @@ async function resolveAgentForLoad(token, query) {
141
132
  * User can override with AIS_AGENT_ID or AIS_AGENT_NAME env vars.
142
133
  */
143
134
  async function resolveAgent(token, fallbackAgentId, fallbackTenantId, preferredAgentId) {
144
- // The harness pin overrides shared preferences and must belong to this tenant.
145
- const envAgentId = harnessIdentity?.agentId;
135
+ // This session's own identity overrides shared preferences and must belong to this tenant.
136
+ const envAgentId = sessionIdentity?.agentId;
146
137
  if (envAgentId) {
147
138
  const agents = await listAllAgents(token);
148
139
  try {
@@ -202,20 +193,22 @@ const MISSING_CREDENTIAL_HELP = 'Missing AIS credential: no authenticated aismem
202
193
  'Local sign-in can use ~/.aismemory/credentials.json. ' +
203
194
  'Railway is an operator-only last resort for locating an existing scoped key; client surfaces never fetch AIS_SERVICE_KEY or DATABASE_URL.';
204
195
  /**
205
- * Bond URL/code surfaced when a tool is called before the user has approved
206
- * the device flow. Caller catches this and returns a friendly error to the MCP
207
- * client so the agent can show the URL inline in chat.
196
+ * Bond URL surfaced when a tool is called before the user has approved the
197
+ * device flow. Caller catches this and returns the text to the MCP client
198
+ * verbatim. The link already carries the user code, so the agent's whole job
199
+ * is to relay that one link — credential fallbacks stay out of this message
200
+ * (CORBOT-899790F0).
208
201
  */
209
202
  class BondPendingError extends Error {
210
203
  verificationUri;
211
204
  userCode;
212
205
  bondPending = true;
213
206
  constructor(verificationUri, userCode) {
214
- super(`Memory bond required — ask your human to approve now.\n\n${MISSING_CREDENTIAL_HELP}\n\n` +
215
- `Visit: ${verificationUri}\n` +
216
- `Code: ${userCode}\n\n` +
217
- `Enter your email and tap Approve, then retry this tool. Takes about 30 seconds.\n` +
218
- `Heads up: this code expires in 30 minutes, and unclaimed agents (and their memories) are archived after 7 days.`);
207
+ super(`Memory bond required. Reply to your human with only this bonding link, on its own line:\n\n` +
208
+ `${verificationUri}\n\n` +
209
+ `Send nothing else: not this message, not a code, not a key, not a command, not JSON. ` +
210
+ `Your human opens the link, signs in, and presses Bond. When they say it is done, retry this tool.\n` +
211
+ `The link expires in 30 minutes. An agent nobody bonds is archived after 7 days, with its memories.`);
219
212
  this.verificationUri = verificationUri;
220
213
  this.userCode = userCode;
221
214
  this.name = 'BondPendingError';
@@ -224,7 +217,17 @@ class BondPendingError extends Error {
224
217
  async function startDeviceFlow(priorProvisionalToken) {
225
218
  emitTelemetry({ event: 'auth.device_flow.start' });
226
219
  stderr('Starting authentication...');
227
- const authRes = (await aisPost('/v1/oauth/device/authorize', { name: `agent-${homedir().split('/').pop() ?? 'user'}` }, priorProvisionalToken));
220
+ // CORBOT-8F96A8EF: when this process has no in-flight flow of its own,
221
+ // fall back to the provisional token left on disk by an earlier process.
222
+ // Without this the token was only ever held in a module-level variable, so
223
+ // every respawn of a stdio server signed in with nothing to present and
224
+ // the server dutifully minted another agent. Sending it lets the server
225
+ // reuse the SAME provisional agent (tryReuseProvisional); if the token is
226
+ // stale or already bonded the server ignores it and mints, exactly as
227
+ // before — so this can only reduce the number of agents, never break a
228
+ // sign-in.
229
+ const carriedToken = priorProvisionalToken ?? loadPendingDeviceFlow()?.provisionalToken;
230
+ const authRes = (await aisPost('/v1/oauth/device/authorize', { name: `agent-${homedir().split('/').pop() ?? 'user'}` }, carriedToken));
228
231
  if (!authRes.success || !authRes.data) {
229
232
  throw new Error(`Authentication failed: ${authRes.error?.message ?? 'Unknown error'}`);
230
233
  }
@@ -237,6 +240,14 @@ async function startDeviceFlow(priorProvisionalToken) {
237
240
  stderr(` Code : ${user_code}`);
238
241
  stderr(' Code expires in 30 min; unclaimed agents are archived after 7 days.');
239
242
  stderr('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
243
+ // Persist before returning, so a process that dies between the bond URL
244
+ // being printed and the human clicking it does not strand this agent.
245
+ savePendingDeviceFlow({
246
+ provisionalToken: provisional_token,
247
+ agentId: agent_id,
248
+ provisionalTenantId: provisional_tenant_id,
249
+ startedAt: new Date().toISOString(),
250
+ });
240
251
  const flow = {
241
252
  device_code,
242
253
  user_code,
@@ -313,6 +324,16 @@ flowRef) {
313
324
  flowRef.user_code = newHandle.user_code;
314
325
  flowRef.provisional_token = authRes.data.provisional_token;
315
326
  }
327
+ // Keep the on-disk copy in step with the in-memory one (CORBOT-8F96A8EF).
328
+ // A code that expires mid-flow issues a fresh provisional token; if the
329
+ // process then dies, the stale token on disk would be rejected on the
330
+ // next run and mint a new agent — the orphan this fix exists to stop.
331
+ savePendingDeviceFlow({
332
+ provisionalToken: authRes.data.provisional_token,
333
+ agentId: authRes.data.agent_id,
334
+ provisionalTenantId: authRes.data.provisional_tenant_id,
335
+ startedAt: new Date().toISOString(),
336
+ });
316
337
  stderr('');
317
338
  stderr('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
318
339
  stderr(' aismemory — device code refreshed');
@@ -360,28 +381,37 @@ flowRef) {
360
381
  // reconnected silently. migrateCredentialsFileIfNeeded() (agent-preference.ts)
361
382
  // is what later removes this token from disk, once a key gets enrolled.
362
383
  if (hasEnrolledKey()) {
363
- if (!harnessIdentity)
384
+ if (!sessionIdentity)
364
385
  saveAgentPreference({ agentId: creds.agentId, tenantId: creds.tenantId });
365
386
  }
366
- else if (!harnessIdentity) {
387
+ else if (!sessionIdentity) {
367
388
  saveOAuthCredentials(creds);
368
389
  }
390
+ // The flow is finished: this agent is bonded and reachable by id from the
391
+ // credentials/preference files. Leaving the provisional token on disk
392
+ // would let a later sign-in present a token for an already-bonded agent,
393
+ // which the server rejects — sending it back to minting.
394
+ clearPendingDeviceFlow();
369
395
  stderr('Authenticated successfully!');
370
396
  return creds;
371
397
  }
372
398
  let agentContext = null;
373
399
  const activityTracker = new ActivityTracker();
374
400
  // ── MCP Tool definitions ────────────────────────────────
401
+ // Appended to the tools an agent calls first, so the relay-only bonding rule
402
+ // is visible from the tool schema itself, not only the README (CORBOT-899790F0).
403
+ const BOND_LINK_NOTE = ' If this agent is not bonded yet, the result is a bonding link: reply to your human with only that link, nothing else.';
375
404
  const tools = [
376
405
  {
377
406
  name: 'remember',
378
- description: 'Store a memory. Types: fact, event, lesson, context, goal, task.',
407
+ description: 'Store a memory. Types: fact, event, lesson, context, goal, task.' + BOND_LINK_NOTE,
379
408
  inputSchema: {
380
409
  type: 'object',
381
410
  properties: {
382
411
  content: { type: 'string', description: 'The memory content to store' },
383
412
  type: { type: 'string', enum: ['fact', 'event', 'lesson', 'context', 'goal', 'task'], description: 'Memory type' },
384
413
  importance: { type: 'number', minimum: 0, maximum: 1, description: 'Importance (0-1, default 0.5)' },
414
+ tags: { type: 'array', items: { type: 'string' }, description: 'Optional tags to filter on later, e.g. ["work", "acme"]. At most 20; each 1-64 characters of a-z 0-9 . _ : - (stored lower-case). Tags are a filter, not a permission — use separate agents to wall off e.g. work from personal.' },
385
415
  },
386
416
  required: ['content'],
387
417
  },
@@ -389,13 +419,17 @@ const tools = [
389
419
  {
390
420
  name: 'recall',
391
421
  description: "Search memories by natural language query. Set principalAgentId to search a DIFFERENT " +
392
- "agent's memories under a 'memory:read' grant from that agent's owner.",
422
+ "agent's memories under a 'memory:read' grant from that agent's owner. " +
423
+ "Each memory carries createdAt, updatedAt (its last edit), and tags. " +
424
+ "Memories superseded by a newer one are left out; to list or page through every memory, " +
425
+ "use the REST list route described in the aismemory README (cursor paging, sortBy=updatedAt, includeSuperseded=true)." + BOND_LINK_NOTE,
393
426
  inputSchema: {
394
427
  type: 'object',
395
428
  properties: {
396
429
  query: { type: 'string', description: 'Search query' },
397
430
  limit: { type: 'number', description: 'Max results (default 10)' },
398
431
  type: { type: 'string', enum: ['fact', 'event', 'lesson', 'context', 'goal', 'task'], description: 'Filter by type' },
432
+ tags: { type: 'array', items: { type: 'string' }, description: 'Only memories carrying ALL of these tags. A hard filter: results are never widened past it.' },
399
433
  principalAgentId: { type: 'string', description: "Optional: search ANOTHER agent's memories instead of your own (its agent UUID). Requires that agent's owner to have granted you the 'memory:read' scope and that you share a tenant; otherwise the call is refused with 403. Read-only — your own identity and write access are unchanged." },
400
434
  },
401
435
  required: ['query'],
@@ -403,13 +437,13 @@ const tools = [
403
437
  },
404
438
  {
405
439
  name: 'whoami',
406
- description: 'Get the current agent identity — name, DID, status, description.',
440
+ description: 'Get the current agent identity — name, DID, status, description.' + BOND_LINK_NOTE,
407
441
  inputSchema: { type: 'object', properties: {} },
408
442
  },
409
443
  {
410
444
  name: 'full_context',
411
445
  description: 'Load the full agent context — identity, personality, goals, facts, skills, and last handoff. ' +
412
- "Set principalAgentId to load ANOTHER agent's context under a 'memory:read' grant.",
446
+ "Set principalAgentId to load ANOTHER agent's context under a 'memory:read' grant." + BOND_LINK_NOTE,
413
447
  inputSchema: {
414
448
  type: 'object',
415
449
  properties: {
@@ -431,6 +465,8 @@ const tools = [
431
465
  properties: {
432
466
  summary: { type: 'string', description: 'Summary of what was accomplished this session' },
433
467
  keyLearnings: { type: 'array', items: { type: 'string' }, description: 'Key learnings to carry forward' },
468
+ docPath: { type: 'string', description: 'Path of a handoff document written this session, saved with the package' },
469
+ docPrUrl: { type: 'string', description: 'Link to the pull request that carries the handoff document' },
434
470
  },
435
471
  },
436
472
  },
@@ -624,6 +660,18 @@ const tools = [
624
660
  required: ['goalId', 'success'],
625
661
  },
626
662
  },
663
+ // Owner-only lifecycle (CORBOT-BA5AADB0): archive, plus the agent_delete
664
+ // alias, which archives too. Key modes refuse both (agent-key-mode.ts).
665
+ {
666
+ name: 'agent_archive',
667
+ description: AGENT_ARCHIVE_DESCRIPTION,
668
+ inputSchema: AGENT_ARCHIVE_INPUT_SCHEMA,
669
+ },
670
+ {
671
+ name: 'agent_delete',
672
+ description: AGENT_DELETE_ALIAS_DESCRIPTION,
673
+ inputSchema: AGENT_ARCHIVE_INPUT_SCHEMA,
674
+ },
627
675
  ];
628
676
  // ── MCP Server ──────────────────────────────────────────
629
677
  /**
@@ -660,61 +708,15 @@ async function runWorkspaceKeyModeServer(config) {
660
708
  server.setRequestHandler(GetPromptRequestSchema, async (request) => {
661
709
  throw new Error(`Unknown prompt: ${request.params.name}`);
662
710
  });
663
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
664
- const { name, arguments: args } = request.params;
665
- const rawArgs = args ?? {};
666
- activityTracker.recordToolUse(name);
667
- const toolStartedAt = Date.now();
668
- try {
669
- const callConfig = activeConfig;
670
- if (requiresIdentityGate(name, rawArgs)) {
671
- await verifyPinnedIdentity(harnessIdentity, callConfig.agentId, () => dispatchToolWorkspaceKeyMode(AIS_URL, callConfig, 'whoami', {}));
672
- }
673
- const result = await dispatchToolWorkspaceKeyMode(AIS_URL, callConfig, name, rawArgs);
674
- if (result === UNKNOWN_TOOL_WORKSPACE_KEY_MODE) {
675
- return {
676
- content: [{ type: 'text', text: JSON.stringify({ error: `Unknown tool: ${name}` }) }],
677
- isError: true,
678
- };
679
- }
680
- // agent_load: update the active agentId for subsequent calls.
681
- if (name === 'agent_load' &&
682
- typeof result === 'object' &&
683
- result !== null &&
684
- 'switched' in result &&
685
- result.switched) {
686
- const switchResult = result;
687
- if (harnessIdentity) {
688
- await verifyPinnedIdentity(harnessIdentity, switchResult.agentId, () => dispatchToolWorkspaceKeyMode(AIS_URL, { ...callConfig, agentId: switchResult.agentId }, 'whoami', {}));
689
- }
690
- activeConfig = { ...activeConfig, agentId: switchResult.agentId };
691
- stderr(`Switched active agent to ${switchResult.agentId}`);
692
- }
693
- emitTelemetry({
694
- event: 'tool.call',
695
- tool: name,
696
- agentIdHash: hashAgentId(activeConfig.agentId),
697
- latencyMs: Date.now() - toolStartedAt,
698
- status: 'ok',
699
- });
700
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
701
- }
702
- catch (error) {
703
- const message = error instanceof Error ? error.message : 'Unknown error';
704
- emitTelemetry({
705
- event: 'tool.call',
706
- tool: name,
707
- agentIdHash: hashAgentId(activeConfig.agentId),
708
- latencyMs: Date.now() - toolStartedAt,
709
- status: 'error',
710
- error: message,
711
- });
712
- return {
713
- content: [{ type: 'text', text: JSON.stringify({ error: message }) }],
714
- isError: true,
715
- };
716
- }
717
- });
711
+ server.setRequestHandler(CallToolRequestSchema, createWorkspaceKeyCallToolHandler({
712
+ aisUrl: AIS_URL,
713
+ activityTracker,
714
+ getSessionIdentity: () => sessionIdentity,
715
+ setSessionIdentity: (next) => { sessionIdentity = next; },
716
+ getActiveConfig: () => activeConfig,
717
+ setActiveConfig: (next) => { activeConfig = next; },
718
+ stderr,
719
+ }));
718
720
  const transport = new StdioServerTransport();
719
721
  await server.connect(transport);
720
722
  stderr(`MCP server running (workspace key mode). Tools: ${tools.map((t) => t.name).join(', ')}`);
@@ -738,47 +740,12 @@ async function runAgentKeyModeServer(config) {
738
740
  server.setRequestHandler(GetPromptRequestSchema, async (request) => {
739
741
  throw new Error(`Unknown prompt: ${request.params.name}`);
740
742
  });
741
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
742
- const { name, arguments: args } = request.params;
743
- const rawArgs = args ?? {};
744
- activityTracker.recordToolUse(name);
745
- const toolStartedAt = Date.now();
746
- try {
747
- if (requiresIdentityGate(name, rawArgs)) {
748
- await verifyPinnedIdentity(harnessIdentity, config.agentId, () => dispatchToolKeyMode(AIS_URL, config, 'whoami', {}));
749
- }
750
- const result = await dispatchToolKeyMode(AIS_URL, config, name, rawArgs);
751
- if (result === UNKNOWN_TOOL_KEY_MODE) {
752
- return {
753
- content: [{ type: 'text', text: JSON.stringify({ error: `Unknown tool: ${name}` }) }],
754
- isError: true,
755
- };
756
- }
757
- emitTelemetry({
758
- event: 'tool.call',
759
- tool: name,
760
- agentIdHash: hashAgentId(config.agentId),
761
- latencyMs: Date.now() - toolStartedAt,
762
- status: 'ok',
763
- });
764
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
765
- }
766
- catch (error) {
767
- const message = error instanceof Error ? error.message : 'Unknown error';
768
- emitTelemetry({
769
- event: 'tool.call',
770
- tool: name,
771
- agentIdHash: hashAgentId(config.agentId),
772
- latencyMs: Date.now() - toolStartedAt,
773
- status: 'error',
774
- error: message,
775
- });
776
- return {
777
- content: [{ type: 'text', text: JSON.stringify({ error: message }) }],
778
- isError: true,
779
- };
780
- }
781
- });
743
+ server.setRequestHandler(CallToolRequestSchema, createAgentKeyCallToolHandler({
744
+ aisUrl: AIS_URL,
745
+ activityTracker,
746
+ config,
747
+ getSessionIdentity: () => sessionIdentity,
748
+ }));
782
749
  const transport = new StdioServerTransport();
783
750
  await server.connect(transport);
784
751
  stderr(`MCP server running (agent API key mode). Tools: ${tools.map((t) => t.name).join(', ')}`);
@@ -797,16 +764,17 @@ async function main() {
797
764
  await (await import('./cli/doctor.js')).main(process.argv.slice(3));
798
765
  return; // doctor.ts calls process.exit() itself
799
766
  }
800
- if (process.argv[2] === 'install') {
801
- const source = optionValue(process.argv.slice(3), '--manifest');
802
- if (!source)
803
- throw new Error('Usage: aismemory install --manifest <file>');
804
- const pin = installHarnessIdentity(source);
805
- process.stdout.write(`Installed identity pin for ${pin.harness}: ${pin.agentId}. Launch: aismemory --harness ${pin.harness}\nVerify: aismemory doctor --harness ${pin.harness}\n`);
767
+ if (process.argv[2] === 'pull') {
768
+ await (await import('./cli/pull-memory.js')).main(process.argv.slice(3));
806
769
  return;
807
770
  }
808
- harnessIdentity = loadHarnessIdentity();
809
- const adapterEnv = { ...process.env, AIS_AGENT_ID: harnessIdentity?.agentId };
771
+ // CORBOT-7B8A1D16: ask the npm registry, at most about once a day, whether a
772
+ // newer aismemory has been published. Deliberately not awaited — startup must
773
+ // not wait on a third-party registry, and the answer is only needed later,
774
+ // when whoami or full_context is called. Resolves to a skip when offline.
775
+ void startVersionCheck({ currentVersion: PACKAGE_VERSION, onUpdateAvailable: stderr });
776
+ sessionIdentity = loadSessionIdentity();
777
+ const adapterEnv = { ...process.env, AIS_AGENT_ID: sessionIdentity?.agentId };
810
778
  // Workspace key auth mode (CORBOT-3103ED88) — multi-agent workspace
811
779
  // credentials. Checked FIRST: workspace key -> agent API key -> device flow.
812
780
  let workspaceKeyConfig;
@@ -898,7 +866,7 @@ async function main() {
898
866
  const claims = decodeJwtClaims(token);
899
867
  const tenantId = claims.tenantId ?? '';
900
868
  const resolved = await resolveAgent(token, '', tenantId, loadAgentPreference()?.agentId);
901
- if (!harnessIdentity)
869
+ if (!sessionIdentity)
902
870
  saveAgentPreference({ agentId: resolved.agentId, tenantId: resolved.tenantId || tenantId });
903
871
  return { agentId: resolved.agentId, tenantId: resolved.tenantId || tenantId };
904
872
  },
@@ -1022,7 +990,7 @@ async function main() {
1022
990
  }
1023
991
  /** Identity hydration, prompt refresh, and auto-handoff — all post-auth. */
1024
992
  async function runPostAuth(credentials) {
1025
- const c = { ...credentials, agentId: harnessIdentity?.agentId ?? credentials.agentId };
993
+ const c = { ...credentials, agentId: sessionIdentity?.agentId ?? credentials.agentId };
1026
994
  // Auto-enroll a DID key so re-auth stays silent after the 7-day bond
1027
995
  // token expires. Without this, key enrollment was a manual opt-in step
1028
996
  // (`enable-key-auth`) and every normal user was re-prompted to bond at
@@ -1050,7 +1018,7 @@ async function main() {
1050
1018
  setupAutoHandoff(AIS_URL, c.agentId, c.token, activityTracker, () => {
1051
1019
  stopRefreshTimer();
1052
1020
  stderr('Session handoff saved');
1053
- }, () => verifyPinnedIdentity(harnessIdentity, c.agentId, () => aisGet(`/v1/agents/${c.agentId}`, c.token)));
1021
+ }, () => verifyPinnedIdentity(sessionIdentity, c.agentId, () => aisGet(`/v1/agents/${c.agentId}`, c.token)));
1054
1022
  }
1055
1023
  catch (e) {
1056
1024
  stderr(`Post-auth setup failed: ${e instanceof Error ? e.message : String(e)}`);
@@ -1074,259 +1042,25 @@ async function main() {
1074
1042
  }],
1075
1043
  };
1076
1044
  });
1077
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
1078
- const { name, arguments: args } = request.params;
1079
- const rawArgs = args ?? {};
1080
- activityTracker.recordToolUse(name);
1081
- const toolStartedAt = Date.now();
1082
- try {
1083
- if (requiresIdentityGate(name, rawArgs)) {
1084
- assertPinnedAgent(harnessIdentity, harnessIdentity?.agentId ?? 'unselected');
1085
- }
1086
- const UNKNOWN_TOOL = Symbol('unknown-tool');
1087
- const dispatchTool = async (c) => {
1088
- const token = c.token;
1089
- // agentId is mutable across `agent_load` calls within this session.
1090
- let agentId = harnessIdentity?.agentId ?? c.agentId;
1091
- if (requiresIdentityGate(name, rawArgs)) {
1092
- await verifyPinnedIdentity(harnessIdentity, agentId, () => aisGet(`/v1/agents/${agentId}`, token));
1093
- }
1094
- let result;
1095
- switch (name) {
1096
- case 'remember': {
1097
- const rememberArgs = parseRememberArgs(rawArgs);
1098
- result = await aisPost(`/v1/agents/${agentId}/memory`, {
1099
- content: rememberArgs.content,
1100
- type: rememberArgs.type,
1101
- importance: rememberArgs.importance,
1102
- }, token);
1103
- activityTracker.recordMemoryStored(rememberArgs.content);
1104
- break;
1105
- }
1106
- case 'recall': {
1107
- const recallArgs = parseRecallArgs(rawArgs);
1108
- const params = new URLSearchParams();
1109
- params.set('query', recallArgs.query);
1110
- if (recallArgs.limit !== undefined)
1111
- params.set('limit', String(recallArgs.limit));
1112
- if (recallArgs.type !== undefined)
1113
- params.set('types', recallArgs.type);
1114
- if (recallArgs.principalAgentId !== undefined) {
1115
- params.set('principalAgentId', recallArgs.principalAgentId);
1116
- }
1117
- result = await aisGet(`/v1/agents/${agentId}/memory?${params}`, token);
1118
- break;
1119
- }
1120
- case 'whoami':
1121
- parseEmptyToolArgs(rawArgs);
1122
- result = await aisGet(`/v1/agents/${agentId}`, token);
1123
- break;
1124
- case 'full_context': {
1125
- const fcArgs = parseFullContextArgs(rawArgs);
1126
- const fcParams = new URLSearchParams();
1127
- if (fcArgs.principalAgentId !== undefined)
1128
- fcParams.set('principalAgentId', fcArgs.principalAgentId);
1129
- if (fcArgs.memoriesLimit !== undefined)
1130
- fcParams.set('memoriesLimit', String(fcArgs.memoriesLimit));
1131
- if (fcArgs.view !== undefined)
1132
- fcParams.set('view', fcArgs.view);
1133
- const fcQs = fcParams.toString();
1134
- const fcPath = `/v1/agents/${agentId}/full-context${fcQs ? `?${fcQs}` : ''}`;
1135
- result = await aisGet(fcPath, token);
1136
- break;
1137
- }
1138
- case 'dream':
1139
- parseEmptyToolArgs(rawArgs);
1140
- result = await aisPost(`/v1/agents/${agentId}/dream`, {}, token);
1141
- break;
1142
- case 'handoff': {
1143
- const handoffArgs = parseHandoffArgs(rawArgs);
1144
- result = await aisPost(`/v1/agents/${agentId}/handoff`, {
1145
- summary: handoffArgs.summary ?? 'Session ended',
1146
- keyLearnings: handoffArgs.keyLearnings ?? [],
1147
- }, token);
1148
- break;
1149
- }
1150
- case 'agent_load': {
1151
- const { query, memoriesLimit } = parseAgentLoadArgs(rawArgs);
1152
- const target = await resolveAgentForLoad(token, query);
1153
- if (harnessIdentity)
1154
- assertPinnedAgent(harnessIdentity, target.id);
1155
- persistClaudeSessionAgentName(process.env.CLAUDE_SESSION_ID, target.name);
1156
- agentId = target.id;
1157
- // Persist the switch so subsequent tool calls use the new agent.
1158
- // saveAgentPreference() MUST be called here so that a process
1159
- // restart (silent MCP reconnect) restores the switched agent from
1160
- // disk instead of silently reverting to the default
1161
- // (CORBOT-497C7FA3). Ids only — no token is ever written to disk.
1162
- // sessionAgentOverride MUST also be set so that a key-enrolled
1163
- // TokenManager doesn't clobber this switch on the very next
1164
- // ensureCredentials() call (the manager resolves identity once
1165
- // and never re-resolves it).
1166
- if (creds) {
1167
- creds = { ...creds, agentId };
1168
- sessionAgentOverride = { agentId, tenantId: creds.tenantId };
1169
- if (!harnessIdentity)
1170
- saveAgentPreference({ agentId, tenantId: creds.tenantId });
1171
- }
1172
- agentContext = await hydrateIdentity(AIS_URL, agentId, token, memoriesLimit);
1173
- applyMemoryRefresh(agentId, token);
1174
- server.notification({ method: 'notifications/prompts/list_changed' });
1175
- stderr(`agent_load: switched to "${target.name}" (${target.id})`);
1176
- result = {
1177
- success: true,
1178
- agentId: target.id,
1179
- name: target.name,
1180
- did: agentContext?.identity.did ?? null,
1181
- status: target.status,
1182
- memoriesLoaded: agentContext?.recentMemories.length ?? 0,
1183
- goalsLoaded: agentContext?.goals.length ?? 0,
1184
- };
1185
- break;
1186
- }
1187
- case 'inbox_send': {
1188
- const inboxSendArgs = parseInboxSendArgs(rawArgs);
1189
- result = await aisPost(`/v1/agents/${agentId}/inbox`, {
1190
- recipientAgentId: inboxSendArgs.recipientAgentId,
1191
- kind: inboxSendArgs.kind,
1192
- body: inboxSendArgs.body,
1193
- ...(inboxSendArgs.threadId !== undefined ? { threadId: inboxSendArgs.threadId } : {}),
1194
- ...(inboxSendArgs.taskId !== undefined ? { taskId: inboxSendArgs.taskId } : {}),
1195
- ...(inboxSendArgs.inReplyTo !== undefined ? { inReplyTo: inboxSendArgs.inReplyTo } : {}),
1196
- ...(inboxSendArgs.messageId !== undefined ? { messageId: inboxSendArgs.messageId } : {}),
1197
- ...(inboxSendArgs.ttlSeconds !== undefined ? { ttlSeconds: inboxSendArgs.ttlSeconds } : {}),
1198
- }, token);
1199
- break;
1200
- }
1201
- case 'inbox_read': {
1202
- const inboxReadArgs = parseInboxReadArgs(rawArgs);
1203
- const inboxParams = new URLSearchParams();
1204
- if (inboxReadArgs.cursor !== undefined)
1205
- inboxParams.set('cursor', inboxReadArgs.cursor);
1206
- inboxParams.set('limit', String(inboxReadArgs.limit));
1207
- inboxParams.set('unreadOnly', String(inboxReadArgs.unreadOnly));
1208
- result = await readInboxAndMaybeAck(inboxReadArgs, () => aisGet(`/v1/agents/${agentId}/inbox?${inboxParams}`, token), (messageIds) => aisPost(`/v1/agents/${agentId}/inbox/ack`, { messageIds }, token));
1209
- break;
1210
- }
1211
- case 'inbox_ack': {
1212
- const inboxAckArgs = parseInboxAckArgs(rawArgs);
1213
- result = await aisPost(`/v1/agents/${agentId}/inbox/ack`, { messageIds: inboxAckArgs.messageIds }, token);
1214
- break;
1215
- }
1216
- case 'goal_create': {
1217
- const goalCreateArgs = parseGoalCreateArgs(rawArgs);
1218
- result = await aisPost(`/v1/agents/${agentId}/goals`, {
1219
- title: goalCreateArgs.title,
1220
- description: goalCreateArgs.description,
1221
- ...(goalCreateArgs.successCriteria !== undefined ? { successCriteria: goalCreateArgs.successCriteria } : {}),
1222
- ...(goalCreateArgs.parentGoalId !== undefined ? { parentGoalId: goalCreateArgs.parentGoalId } : {}),
1223
- }, token);
1224
- break;
1225
- }
1226
- case 'goal_list': {
1227
- const goalListArgs = parseGoalListArgs(rawArgs);
1228
- const goalParams = new URLSearchParams();
1229
- if (goalListArgs.status !== undefined)
1230
- goalParams.set('status', goalListArgs.status);
1231
- if (goalListArgs.page !== undefined)
1232
- goalParams.set('page', String(goalListArgs.page));
1233
- goalParams.set('limit', String(goalListArgs.limit));
1234
- if (goalListArgs.parentGoalId !== undefined)
1235
- goalParams.set('parentGoalId', goalListArgs.parentGoalId);
1236
- result = await aisGet(`/v1/agents/${agentId}/goals?${goalParams}`, token);
1237
- break;
1238
- }
1239
- case 'goal_update_progress': {
1240
- const goalProgressArgs = parseGoalUpdateProgressArgs(rawArgs);
1241
- result = await aisPatch(`/v1/agents/${agentId}/goals/${goalProgressArgs.goalId}`, {
1242
- progress: goalProgressArgs.progress,
1243
- }, token);
1244
- break;
1245
- }
1246
- case 'goal_complete': {
1247
- const goalCompleteArgs = parseGoalCompleteArgs(rawArgs);
1248
- result = await aisPost(`/v1/agents/${agentId}/goals/${goalCompleteArgs.goalId}/complete`, {
1249
- success: goalCompleteArgs.success,
1250
- ...(goalCompleteArgs.notes !== undefined ? { notes: goalCompleteArgs.notes } : {}),
1251
- ...(goalCompleteArgs.lessonsLearned !== undefined ? { lessonsLearned: goalCompleteArgs.lessonsLearned } : {}),
1252
- }, token);
1253
- break;
1254
- }
1255
- default:
1256
- emitTelemetry({
1257
- event: 'tool.call',
1258
- tool: name,
1259
- agentIdHash: hashAgentId(agentId),
1260
- latencyMs: Date.now() - toolStartedAt,
1261
- status: 'error',
1262
- error: `Unknown tool: ${name}`,
1263
- });
1264
- return UNKNOWN_TOOL;
1265
- }
1266
- return result;
1267
- };
1268
- let result = await dispatchTool(await ensureCredentials());
1269
- if (isAuthErrorResult(result)) {
1270
- // CORBOT-A5BBD580: the server rejected our token mid-session
1271
- // (expired or secret rotated). Clear the cached creds, silently
1272
- // re-auth (key auth when a key file exists), and retry once.
1273
- stderr('AIS rejected the cached token; re-authenticating and retrying once...');
1274
- creds = null;
1275
- // Drop the TokenManager's cached token too — without this, the
1276
- // key-enrolled path's ensureCredentials() would just re-serve the
1277
- // same still-time-valid-but-server-rejected token from `current`
1278
- // instead of re-running challenge/prove (CORBOT-A5BBD580: a JWT
1279
- // rejection mid-session, e.g. AIS rotating JWT_SECRET, must force a
1280
- // real re-auth, not a no-op retry with the same token). Also reset
1281
- // any escalated backoff window (CORBOT-4F7CAA8E / F2b): the server
1282
- // just responded with a 401, so it is reachable right now — a stale
1283
- // backoff window computed from unrelated prior renewal failures must
1284
- // not hold this re-prove behind the getCredentials() gate.
1285
- tokenManager.invalidate({ resetBackoff: true });
1286
- result = await dispatchTool(await ensureCredentials());
1287
- }
1288
- if (result === UNKNOWN_TOOL) {
1289
- return {
1290
- content: [{ type: 'text', text: JSON.stringify({ error: `Unknown tool: ${name}` }) }],
1291
- isError: true,
1292
- };
1293
- }
1294
- emitTelemetry({
1295
- event: 'tool.call',
1296
- tool: name,
1297
- agentIdHash: creds ? hashAgentId(creds.agentId) : undefined,
1298
- latencyMs: Date.now() - toolStartedAt,
1299
- status: 'ok',
1300
- });
1301
- return {
1302
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
1303
- };
1304
- }
1305
- catch (error) {
1306
- const message = error instanceof Error ? error.message : 'Unknown error';
1307
- const agentHash = creds ? hashAgentId(creds.agentId) : undefined;
1308
- emitTelemetry({
1309
- event: 'tool.call',
1310
- tool: name,
1311
- agentIdHash: agentHash,
1312
- latencyMs: Date.now() - toolStartedAt,
1313
- status: 'error',
1314
- error: message,
1315
- });
1316
- // BondPendingError carries human-readable text that the agent should
1317
- // render verbatim — return as plain text rather than JSON.
1318
- if (error instanceof BondPendingError) {
1319
- return {
1320
- content: [{ type: 'text', text: message }],
1321
- isError: true,
1322
- };
1323
- }
1324
- return {
1325
- content: [{ type: 'text', text: JSON.stringify({ error: message }) }],
1326
- isError: true,
1327
- };
1328
- }
1329
- });
1045
+ server.setRequestHandler(CallToolRequestSchema, createOAuthCallToolHandler({
1046
+ aisUrl: AIS_URL,
1047
+ server,
1048
+ activityTracker,
1049
+ getSessionIdentity: () => sessionIdentity,
1050
+ setSessionIdentity: (next) => { sessionIdentity = next; },
1051
+ getCredentials: () => creds,
1052
+ setCredentials: (next) => { creds = next; },
1053
+ setSessionAgentOverride: (next) => { sessionAgentOverride = next; },
1054
+ getAgentContext: () => agentContext,
1055
+ setAgentContext: (next) => { agentContext = next; },
1056
+ ensureCredentials: () => ensureCredentials(),
1057
+ invalidateToken: () => { tokenManager.invalidate({ resetBackoff: true }); },
1058
+ applyMemoryRefresh,
1059
+ resolveAgentForLoad,
1060
+ persistClaudeSessionAgentName,
1061
+ isBondPendingError: (error) => error instanceof BondPendingError,
1062
+ stderr,
1063
+ }));
1330
1064
  // Connect the transport BEFORE any auth so MCP `initialize` returns in <1s.
1331
1065
  const transport = new StdioServerTransport();
1332
1066
  await server.connect(transport);
@@ -1349,6 +1083,9 @@ async function main() {
1349
1083
  else if (creds) {
1350
1084
  stderr('Device key enrolled — deferring session setup to the first key-auth token acquire.');
1351
1085
  }
1086
+ else if (hasEnrolledKey()) {
1087
+ stderr('Using enrolled device key; session starts on first tool call.');
1088
+ }
1352
1089
  else {
1353
1090
  stderr(`${MISSING_CREDENTIAL_HELP} Bond URL will be surfaced on first tool call.`);
1354
1091
  }