aismemory 0.5.1 → 0.5.3
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/dist/__tests__/ais-http-error.test.d.ts +1 -0
- package/dist/__tests__/ais-http-error.test.js +49 -0
- package/dist/__tests__/ais-http-error.test.js.map +1 -0
- package/dist/__tests__/auto-enroll-key-e2e.test.d.ts +1 -0
- package/dist/__tests__/auto-enroll-key-e2e.test.js +116 -0
- package/dist/__tests__/auto-enroll-key-e2e.test.js.map +1 -0
- package/dist/__tests__/auto-enroll-key.test.d.ts +1 -0
- package/dist/__tests__/auto-enroll-key.test.js +165 -0
- package/dist/__tests__/auto-enroll-key.test.js.map +1 -0
- package/dist/__tests__/device-flow-recovery.test.js +9 -5
- package/dist/__tests__/device-flow-recovery.test.js.map +1 -1
- package/dist/__tests__/device-poll.test.d.ts +12 -0
- package/dist/__tests__/device-poll.test.js +191 -0
- package/dist/__tests__/device-poll.test.js.map +1 -0
- package/dist/__tests__/mcp-init-no-auth.test.js +14 -1
- package/dist/__tests__/mcp-init-no-auth.test.js.map +1 -1
- package/dist/__tests__/memory-write-failure.test.d.ts +1 -0
- package/dist/__tests__/memory-write-failure.test.js +148 -0
- package/dist/__tests__/memory-write-failure.test.js.map +1 -0
- package/dist/__tests__/resolve-agent-preference-e2e.test.d.ts +1 -0
- package/dist/__tests__/resolve-agent-preference-e2e.test.js +173 -0
- package/dist/__tests__/resolve-agent-preference-e2e.test.js.map +1 -0
- package/dist/__tests__/resolve-agent-preference.test.d.ts +1 -0
- package/dist/__tests__/resolve-agent-preference.test.js +83 -0
- package/dist/__tests__/resolve-agent-preference.test.js.map +1 -0
- package/dist/ais-http-error.d.ts +26 -0
- package/dist/ais-http-error.js +75 -0
- package/dist/ais-http-error.js.map +1 -0
- package/dist/device-poll.d.ts +51 -0
- package/dist/device-poll.js +77 -0
- package/dist/device-poll.js.map +1 -0
- package/dist/env-agent.d.ts +27 -0
- package/dist/env-agent.js +33 -0
- package/dist/env-agent.js.map +1 -1
- package/dist/index.js +155 -58
- package/dist/index.js.map +1 -1
- package/dist/key-auth.d.ts +30 -0
- package/dist/key-auth.js +77 -1
- package/dist/key-auth.js.map +1 -1
- package/dist/telemetry.d.ts +1 -1
- package/dist/telemetry.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CORBOT-B0FA4A79: resolveAgent() silently reconnects to a different agent
|
|
3
|
+
* after re-auth.
|
|
4
|
+
*
|
|
5
|
+
* After a silent re-auth (or a re-bond), the client picked the FIRST active
|
|
6
|
+
* agent in the tenant instead of the agent the user was previously bonded
|
|
7
|
+
* to. A multi-agent user could be reconnected to a different agent with
|
|
8
|
+
* different memories — to the user this looks like their agent forgot
|
|
9
|
+
* everything.
|
|
10
|
+
*
|
|
11
|
+
* Fix under test: selectAgentForSession() prefers the previously bonded
|
|
12
|
+
* agentId (recovered from the saved credentials file) over first-active,
|
|
13
|
+
* and reports an explicit switch when that agent is gone. Env overrides
|
|
14
|
+
* keep their existing priority.
|
|
15
|
+
*/
|
|
16
|
+
import { describe, it, expect } from 'vitest';
|
|
17
|
+
import { selectAgentForSession } from '../env-agent.js';
|
|
18
|
+
const AGENT_A = { id: 'agent-a', name: 'MyAgent', status: 'active' };
|
|
19
|
+
const AGENT_B = { id: 'agent-b', name: 'OtherAgent', status: 'active' };
|
|
20
|
+
describe('selectAgentForSession', () => {
|
|
21
|
+
it('prefers the previously bonded agent over the first active agent', () => {
|
|
22
|
+
const selection = selectAgentForSession({
|
|
23
|
+
agents: [AGENT_B, AGENT_A], // previously bonded agent listed second
|
|
24
|
+
preferredAgentId: 'agent-a',
|
|
25
|
+
fallbackAgentId: 'agent-new',
|
|
26
|
+
});
|
|
27
|
+
expect(selection.agentId).toBe('agent-a');
|
|
28
|
+
expect(selection.reason).toBe('preferred');
|
|
29
|
+
});
|
|
30
|
+
it('falls back to first-active and reports the switch when the preferred agent is gone', () => {
|
|
31
|
+
const selection = selectAgentForSession({
|
|
32
|
+
agents: [AGENT_B],
|
|
33
|
+
preferredAgentId: 'agent-a',
|
|
34
|
+
fallbackAgentId: 'agent-new',
|
|
35
|
+
});
|
|
36
|
+
expect(selection.agentId).toBe('agent-b');
|
|
37
|
+
expect(selection.reason).toBe('first-active');
|
|
38
|
+
// The switch must be loud enough to log: it explains apparent memory loss.
|
|
39
|
+
expect(selection.notes.join(' ')).toMatch(/agent-a/);
|
|
40
|
+
expect(selection.notes.join(' ')).toMatch(/switch/i);
|
|
41
|
+
});
|
|
42
|
+
it('lets AIS_AGENT_NAME win over the previously bonded agent', () => {
|
|
43
|
+
const selection = selectAgentForSession({
|
|
44
|
+
agents: [AGENT_B, AGENT_A],
|
|
45
|
+
envAgentName: 'otheragent',
|
|
46
|
+
preferredAgentId: 'agent-a',
|
|
47
|
+
fallbackAgentId: 'agent-new',
|
|
48
|
+
});
|
|
49
|
+
expect(selection.agentId).toBe('agent-b');
|
|
50
|
+
expect(selection.reason).toBe('env-name');
|
|
51
|
+
});
|
|
52
|
+
it('keeps existing behavior with no preference: first active agent', () => {
|
|
53
|
+
const selection = selectAgentForSession({
|
|
54
|
+
agents: [AGENT_B, AGENT_A],
|
|
55
|
+
fallbackAgentId: 'agent-new',
|
|
56
|
+
});
|
|
57
|
+
expect(selection.agentId).toBe('agent-b');
|
|
58
|
+
expect(selection.reason).toBe('first-active');
|
|
59
|
+
expect(selection.notes).toHaveLength(0);
|
|
60
|
+
});
|
|
61
|
+
it('uses the fallback (fresh provisional) agent when the tenant has no active agents', () => {
|
|
62
|
+
const selection = selectAgentForSession({
|
|
63
|
+
agents: [],
|
|
64
|
+
preferredAgentId: 'agent-a',
|
|
65
|
+
fallbackAgentId: 'agent-new',
|
|
66
|
+
});
|
|
67
|
+
expect(selection.agentId).toBe('agent-new');
|
|
68
|
+
expect(selection.reason).toBe('fallback');
|
|
69
|
+
});
|
|
70
|
+
it('warns but continues when AIS_AGENT_NAME matches nothing', () => {
|
|
71
|
+
const selection = selectAgentForSession({
|
|
72
|
+
agents: [AGENT_B, AGENT_A],
|
|
73
|
+
envAgentName: 'nosuchagent',
|
|
74
|
+
preferredAgentId: 'agent-a',
|
|
75
|
+
fallbackAgentId: 'agent-new',
|
|
76
|
+
});
|
|
77
|
+
// Name miss falls through to the bonded-agent preference.
|
|
78
|
+
expect(selection.agentId).toBe('agent-a');
|
|
79
|
+
expect(selection.reason).toBe('preferred');
|
|
80
|
+
expect(selection.notes.join(' ')).toMatch(/nosuchagent/);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
//# sourceMappingURL=resolve-agent-preference.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-agent-preference.test.js","sourceRoot":"","sources":["../../src/__tests__/resolve-agent-preference.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed error for non-2xx responses from the Agent Identity Service.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists: aisPost/aisGet previously returned `res.json()` even on a
|
|
5
|
+
* non-2xx status, so a failed memory write (422 flagged content, 429 quota,
|
|
6
|
+
* 409 duplicate, 5xx) was handed back to the agent as an ordinary tool result
|
|
7
|
+
* — and `remember` recorded it as "stored" regardless. The agent believed it
|
|
8
|
+
* had remembered something it had not, which is the worst possible failure
|
|
9
|
+
* mode for a memory product. Throwing this error routes the failure through
|
|
10
|
+
* the MCP catch handler, which marks the tool response `isError: true` so the
|
|
11
|
+
* agent sees an unmistakable failure.
|
|
12
|
+
*
|
|
13
|
+
* Token-rejection bodies (TOKEN_EXPIRED / INVALID_TOKEN) are deliberately NOT
|
|
14
|
+
* thrown by the callers — those feed the silent re-auth-and-retry loop
|
|
15
|
+
* (see isAuthErrorResult in auth-staleness.ts).
|
|
16
|
+
*/
|
|
17
|
+
/** Pull a human-readable message out of an AIS error body, if present. */
|
|
18
|
+
export declare function extractServerMessage(body: unknown): string | null;
|
|
19
|
+
/** Build the agent-facing message for a failed AIS request. */
|
|
20
|
+
export declare function formatAisHttpError(status: number, path: string, body: unknown): string;
|
|
21
|
+
export declare class AisHttpError extends Error {
|
|
22
|
+
readonly status: number;
|
|
23
|
+
readonly path: string;
|
|
24
|
+
readonly body: unknown;
|
|
25
|
+
constructor(status: number, path: string, body: unknown);
|
|
26
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed error for non-2xx responses from the Agent Identity Service.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists: aisPost/aisGet previously returned `res.json()` even on a
|
|
5
|
+
* non-2xx status, so a failed memory write (422 flagged content, 429 quota,
|
|
6
|
+
* 409 duplicate, 5xx) was handed back to the agent as an ordinary tool result
|
|
7
|
+
* — and `remember` recorded it as "stored" regardless. The agent believed it
|
|
8
|
+
* had remembered something it had not, which is the worst possible failure
|
|
9
|
+
* mode for a memory product. Throwing this error routes the failure through
|
|
10
|
+
* the MCP catch handler, which marks the tool response `isError: true` so the
|
|
11
|
+
* agent sees an unmistakable failure.
|
|
12
|
+
*
|
|
13
|
+
* Token-rejection bodies (TOKEN_EXPIRED / INVALID_TOKEN) are deliberately NOT
|
|
14
|
+
* thrown by the callers — those feed the silent re-auth-and-retry loop
|
|
15
|
+
* (see isAuthErrorResult in auth-staleness.ts).
|
|
16
|
+
*/
|
|
17
|
+
/** Pull a human-readable message out of an AIS error body, if present. */
|
|
18
|
+
export function extractServerMessage(body) {
|
|
19
|
+
if (typeof body !== 'object' || body === null) {
|
|
20
|
+
return typeof body === 'string' && body.trim() !== '' ? body : null;
|
|
21
|
+
}
|
|
22
|
+
const b = body;
|
|
23
|
+
if (typeof b.error === 'string' && b.error.trim() !== '')
|
|
24
|
+
return b.error;
|
|
25
|
+
if (typeof b.error === 'object' && b.error !== null) {
|
|
26
|
+
const e = b.error;
|
|
27
|
+
const parts = [];
|
|
28
|
+
if (typeof e.message === 'string' && e.message.trim() !== '')
|
|
29
|
+
parts.push(e.message);
|
|
30
|
+
if (typeof e.code === 'string' && e.code.trim() !== '')
|
|
31
|
+
parts.push(`(${e.code})`);
|
|
32
|
+
if (parts.length > 0)
|
|
33
|
+
return parts.join(' ');
|
|
34
|
+
}
|
|
35
|
+
if (typeof b.message === 'string' && b.message.trim() !== '')
|
|
36
|
+
return b.message;
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
/** A status-specific hint so the agent understands WHY the request failed. */
|
|
40
|
+
function statusHint(status) {
|
|
41
|
+
switch (status) {
|
|
42
|
+
case 409:
|
|
43
|
+
return 'duplicate — this memory already exists';
|
|
44
|
+
case 422:
|
|
45
|
+
return 'content rejected (validation/safety) — try plain ASCII, no smart quotes or arrows';
|
|
46
|
+
case 429:
|
|
47
|
+
return 'quota or rate limit exceeded';
|
|
48
|
+
case 401:
|
|
49
|
+
case 403:
|
|
50
|
+
return 'not authorized';
|
|
51
|
+
default:
|
|
52
|
+
if (status >= 500)
|
|
53
|
+
return 'AIS server error — retry later';
|
|
54
|
+
return 'request rejected';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Build the agent-facing message for a failed AIS request. */
|
|
58
|
+
export function formatAisHttpError(status, path, body) {
|
|
59
|
+
const serverMsg = extractServerMessage(body);
|
|
60
|
+
const detail = serverMsg ? `: ${serverMsg}` : '';
|
|
61
|
+
return `AIS request to ${path} failed — HTTP ${status} (${statusHint(status)})${detail}. The operation did NOT succeed.`;
|
|
62
|
+
}
|
|
63
|
+
export class AisHttpError extends Error {
|
|
64
|
+
status;
|
|
65
|
+
path;
|
|
66
|
+
body;
|
|
67
|
+
constructor(status, path, body) {
|
|
68
|
+
super(formatAisHttpError(status, path, body));
|
|
69
|
+
this.name = 'AisHttpError';
|
|
70
|
+
this.status = status;
|
|
71
|
+
this.path = path;
|
|
72
|
+
this.body = body;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=ais-http-error.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ais-http-error.js","sourceRoot":"","sources":["../src/ais-http-error.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device-flow poll loop — extracted for testability.
|
|
3
|
+
*
|
|
4
|
+
* S2 improvements over the original inline loop:
|
|
5
|
+
* - Throttled heartbeat so the human knows polling is active (one line per
|
|
6
|
+
* HEARTBEAT_INTERVAL_MS, not every 5-second poll).
|
|
7
|
+
* - Coaching message when pending_reason === 'page_open' ("you're on the
|
|
8
|
+
* bond page — click Complete bonding").
|
|
9
|
+
* - Auto-restart on expired_token: re-invokes startDeviceFlow and throws
|
|
10
|
+
* DeviceFlowRestartedError so the caller can update the surfaced URL.
|
|
11
|
+
*/
|
|
12
|
+
/** Minimum milliseconds between heartbeat stderr lines during pending polls. */
|
|
13
|
+
export declare const HEARTBEAT_INTERVAL_MS = 30000;
|
|
14
|
+
/** Thrown when the device code expires and a fresh flow has been initiated. */
|
|
15
|
+
export declare class DeviceFlowRestartedError extends Error {
|
|
16
|
+
readonly newVerificationUri: string;
|
|
17
|
+
readonly newUserCode: string;
|
|
18
|
+
readonly isDeviceFlowRestarted = true;
|
|
19
|
+
constructor(newVerificationUri: string, newUserCode: string);
|
|
20
|
+
}
|
|
21
|
+
export interface PollTokenResponse {
|
|
22
|
+
access_token?: string;
|
|
23
|
+
expires_in?: number;
|
|
24
|
+
error?: string;
|
|
25
|
+
pending_reason?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface DeviceFlowHandle {
|
|
28
|
+
device_code: string;
|
|
29
|
+
verification_uri_complete: string;
|
|
30
|
+
user_code: string;
|
|
31
|
+
}
|
|
32
|
+
export interface PollLoopDeps {
|
|
33
|
+
/** Calls the AIS token endpoint and returns the parsed response. */
|
|
34
|
+
pollOnce: (deviceCode: string) => Promise<PollTokenResponse>;
|
|
35
|
+
/** Initiates a fresh device flow and returns the new handle. */
|
|
36
|
+
startNewFlow: () => Promise<DeviceFlowHandle>;
|
|
37
|
+
/** Writes a line to stderr (the existing `stderr` helper in index.ts). */
|
|
38
|
+
emitStderr: (msg: string) => void;
|
|
39
|
+
/** Returns the current wall-clock time in milliseconds (injectable for tests). */
|
|
40
|
+
nowMs?: () => number;
|
|
41
|
+
/** Overrides the poll sleep duration (injectable for tests). */
|
|
42
|
+
pollIntervalMs?: number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Poll the AIS token endpoint until the device is approved, expired, or denied.
|
|
46
|
+
*
|
|
47
|
+
* Returns the raw token response on success (access_token present).
|
|
48
|
+
* Throws DeviceFlowRestartedError on expired_token (after auto-restart).
|
|
49
|
+
* Throws Error on access_denied or hard deadline.
|
|
50
|
+
*/
|
|
51
|
+
export declare function runPollLoop(handle: DeviceFlowHandle, deps: PollLoopDeps): Promise<PollTokenResponse>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device-flow poll loop — extracted for testability.
|
|
3
|
+
*
|
|
4
|
+
* S2 improvements over the original inline loop:
|
|
5
|
+
* - Throttled heartbeat so the human knows polling is active (one line per
|
|
6
|
+
* HEARTBEAT_INTERVAL_MS, not every 5-second poll).
|
|
7
|
+
* - Coaching message when pending_reason === 'page_open' ("you're on the
|
|
8
|
+
* bond page — click Complete bonding").
|
|
9
|
+
* - Auto-restart on expired_token: re-invokes startDeviceFlow and throws
|
|
10
|
+
* DeviceFlowRestartedError so the caller can update the surfaced URL.
|
|
11
|
+
*/
|
|
12
|
+
/** Minimum milliseconds between heartbeat stderr lines during pending polls. */
|
|
13
|
+
export const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
14
|
+
/** Thrown when the device code expires and a fresh flow has been initiated. */
|
|
15
|
+
export class DeviceFlowRestartedError extends Error {
|
|
16
|
+
newVerificationUri;
|
|
17
|
+
newUserCode;
|
|
18
|
+
isDeviceFlowRestarted = true;
|
|
19
|
+
constructor(newVerificationUri, newUserCode) {
|
|
20
|
+
super(`Device code expired. A new code has been issued.\n` +
|
|
21
|
+
`Visit: ${newVerificationUri}\n` +
|
|
22
|
+
`Code: ${newUserCode}`);
|
|
23
|
+
this.newVerificationUri = newVerificationUri;
|
|
24
|
+
this.newUserCode = newUserCode;
|
|
25
|
+
this.name = 'DeviceFlowRestartedError';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Poll the AIS token endpoint until the device is approved, expired, or denied.
|
|
30
|
+
*
|
|
31
|
+
* Returns the raw token response on success (access_token present).
|
|
32
|
+
* Throws DeviceFlowRestartedError on expired_token (after auto-restart).
|
|
33
|
+
* Throws Error on access_denied or hard deadline.
|
|
34
|
+
*/
|
|
35
|
+
export async function runPollLoop(handle, deps) {
|
|
36
|
+
const { pollOnce, startNewFlow, emitStderr, nowMs = () => Date.now(), pollIntervalMs = 5_000, } = deps;
|
|
37
|
+
const deadline = nowMs() + 30 * 60 * 1000; // 30 min
|
|
38
|
+
let lastHeartbeatAt = 0;
|
|
39
|
+
const device_code = handle.device_code;
|
|
40
|
+
while (nowMs() < deadline) {
|
|
41
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
42
|
+
const resp = await pollOnce(device_code);
|
|
43
|
+
if (resp.access_token) {
|
|
44
|
+
return resp;
|
|
45
|
+
}
|
|
46
|
+
if (resp.error === 'authorization_pending') {
|
|
47
|
+
// Throttled heartbeat
|
|
48
|
+
const now = nowMs();
|
|
49
|
+
if (now - lastHeartbeatAt >= HEARTBEAT_INTERVAL_MS) {
|
|
50
|
+
lastHeartbeatAt = now;
|
|
51
|
+
if (resp.pending_reason === 'page_open') {
|
|
52
|
+
emitStderr("you're on the bond page — click Complete bonding");
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
emitStderr('Waiting for approval...');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (resp.error === 'slow_down') {
|
|
61
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (resp.error === 'expired_token') {
|
|
65
|
+
// Auto-restart: fetch a fresh device code and surface the new URL.
|
|
66
|
+
const newFlow = await startNewFlow();
|
|
67
|
+
throw new DeviceFlowRestartedError(newFlow.verification_uri_complete, newFlow.user_code);
|
|
68
|
+
}
|
|
69
|
+
if (resp.error === 'access_denied') {
|
|
70
|
+
throw new Error('Authorization denied by user');
|
|
71
|
+
}
|
|
72
|
+
// Unknown error — surface it and stop
|
|
73
|
+
throw new Error(`Unexpected poll response: ${resp.error ?? '(no error field)'}`);
|
|
74
|
+
}
|
|
75
|
+
throw new Error('Authentication timed out');
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=device-poll.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device-poll.js","sourceRoot":"","sources":["../src/device-poll.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,gFAAgF;AAChF,MAAM,CAAC,MAAM,qBAAqB,GAAG,MAAM,CAAC;AAE5C,+EAA+E;AAC/E,MAAM,OAAO,wBAAyB,SAAQ,KAAK;IAG/B;IACA;IAHT,qBAAqB,GAAG,IAAI,CAAC;IACtC,YACkB,kBAA0B,EAC1B,WAAmB;QAEnC,KAAK,CACH,oDAAoD;YAClD,UAAU,kBAAkB,IAAI;YAChC,SAAS,WAAW,EAAE,CACzB,CAAC;QAPc,uBAAkB,GAAlB,kBAAkB,CAAQ;QAC1B,gBAAW,GAAX,WAAW,CAAQ;QAOnC,IAAI,CAAC,IAAI,GAAG,0BAA0B,CAAC;IACzC,CAAC;CACF;AA4BD;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,MAAwB,EACxB,IAAkB;IAElB,MAAM,EACJ,QAAQ,EACR,YAAY,EACZ,UAAU,EACV,KAAK,GAAG,GAAW,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,EAChC,cAAc,GAAG,KAAK,GACvB,GAAG,IAAI,CAAC;IAET,MAAM,QAAQ,GAAG,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,SAAS;IACpD,IAAI,eAAe,GAAG,CAAC,CAAC;IACxB,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,CAAC;IAEvC,OAAO,KAAK,EAAE,GAAG,QAAQ,EAAE,CAAC;QAC1B,MAAM,IAAI,OAAO,CAAO,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC;QAE9D,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,WAAW,CAAC,CAAC;QAEzC,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;YACtB,OAAO,IAAI,CAAC;QACd,CAAC;QAED,IAAI,IAAI,CAAC,KAAK,KAAK,uBAAuB,EAAE,CAAC;YAC3C,sBAAsB;YACtB,MAAM,GAAG,GAAG,KAAK,EAAE,CAAC;YACpB,IAAI,GAAG,GAAG,eAAe,IAAI,qBAAqB,EAAE,CAAC;gBACnD,eAAe,GAAG,GAAG,CAAC;gBACtB,IAAI,IAAI,CAAC,cAAc,KAAK,WAAW,EAAE,CAAC;oBACxC,UAAU,CAAC,kDAAkD,CAAC,CAAC;gBACjE,CAAC;qBAAM,CAAC;oBACN,UAAU,CAAC,yBAAyB,CAAC,CAAC;gBACxC,CAAC;YACH,CAAC;YACD,SAAS;QACX,CAAC;QAED,IAAI,IAAI,CAAC,KAAK,KAAK,WAAW,EAAE,CAAC;YAC/B,MAAM,IAAI,OAAO,CAAO,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC;YAC9D,SAAS;QACX,CAAC;QAED,IAAI,IAAI,CAAC,KAAK,KAAK,eAAe,EAAE,CAAC;YACnC,mEAAmE;YACnE,MAAM,OAAO,GAAG,MAAM,YAAY,EAAE,CAAC;YACrC,MAAM,IAAI,wBAAwB,CAChC,OAAO,CAAC,yBAAyB,EACjC,OAAO,CAAC,SAAS,CAClB,CAAC;QACJ,CAAC;QAED,IAAI,IAAI,CAAC,KAAK,KAAK,eAAe,EAAE,CAAC;YACnC,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;QAClD,CAAC;QAED,sCAAsC;QACtC,MAAM,IAAI,KAAK,CAAC,6BAA6B,IAAI,CAAC,KAAK,IAAI,kBAAkB,EAAE,CAAC,CAAC;IACnF,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;AAC9C,CAAC"}
|
package/dist/env-agent.d.ts
CHANGED
|
@@ -8,3 +8,30 @@ export interface TenantAgentSummary {
|
|
|
8
8
|
* Throws with a message suitable for stderr when the id is missing or unknown.
|
|
9
9
|
*/
|
|
10
10
|
export declare function assertEnvAgentInTenant(envAgentId: string, agents: TenantAgentSummary[]): TenantAgentSummary;
|
|
11
|
+
export interface AgentSelection {
|
|
12
|
+
agentId: string;
|
|
13
|
+
reason: 'env-name' | 'preferred' | 'first-active' | 'fallback';
|
|
14
|
+
/** Human-readable warnings for the caller to log (name misses, silent-switch alerts). */
|
|
15
|
+
notes: string[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Pick which agent this session connects to (CORBOT-B0FA4A79).
|
|
19
|
+
*
|
|
20
|
+
* Priority: AIS_AGENT_NAME match > previously bonded agent > first active >
|
|
21
|
+
* fallback (fresh provisional agent when the tenant has none). The bonded
|
|
22
|
+
* preference is the load-bearing part: without it, a re-auth reconnected a
|
|
23
|
+
* multi-agent user to whichever agent the server listed first — a different
|
|
24
|
+
* agent with different memories, which reads as memory loss. When the bonded
|
|
25
|
+
* agent is gone, the switch is reported in `notes` so it is never silent.
|
|
26
|
+
* (AIS_AGENT_ID is asserted separately by the caller and wins over all of
|
|
27
|
+
* these.)
|
|
28
|
+
*/
|
|
29
|
+
export declare function selectAgentForSession(opts: {
|
|
30
|
+
/** Active agents in the tenant, in server order. */
|
|
31
|
+
agents: TenantAgentSummary[];
|
|
32
|
+
envAgentName?: string;
|
|
33
|
+
/** Agent id from the previous bond (saved credentials), if any. */
|
|
34
|
+
preferredAgentId?: string;
|
|
35
|
+
/** Fresh provisional agent id to use when the tenant has no active agents. */
|
|
36
|
+
fallbackAgentId: string;
|
|
37
|
+
}): AgentSelection;
|
package/dist/env-agent.js
CHANGED
|
@@ -11,4 +11,37 @@ export function assertEnvAgentInTenant(envAgentId, agents) {
|
|
|
11
11
|
throw new Error(`AIS_AGENT_ID="${envAgentId}" is not a valid agent in this tenant. ` +
|
|
12
12
|
`Available: ${available || '(none)'}`);
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Pick which agent this session connects to (CORBOT-B0FA4A79).
|
|
16
|
+
*
|
|
17
|
+
* Priority: AIS_AGENT_NAME match > previously bonded agent > first active >
|
|
18
|
+
* fallback (fresh provisional agent when the tenant has none). The bonded
|
|
19
|
+
* preference is the load-bearing part: without it, a re-auth reconnected a
|
|
20
|
+
* multi-agent user to whichever agent the server listed first — a different
|
|
21
|
+
* agent with different memories, which reads as memory loss. When the bonded
|
|
22
|
+
* agent is gone, the switch is reported in `notes` so it is never silent.
|
|
23
|
+
* (AIS_AGENT_ID is asserted separately by the caller and wins over all of
|
|
24
|
+
* these.)
|
|
25
|
+
*/
|
|
26
|
+
export function selectAgentForSession(opts) {
|
|
27
|
+
const notes = [];
|
|
28
|
+
if (opts.agents.length === 0) {
|
|
29
|
+
return { agentId: opts.fallbackAgentId, reason: 'fallback', notes };
|
|
30
|
+
}
|
|
31
|
+
if (opts.envAgentName) {
|
|
32
|
+
const match = opts.agents.find((a) => a.name.toLowerCase() === opts.envAgentName.toLowerCase());
|
|
33
|
+
if (match) {
|
|
34
|
+
return { agentId: match.id, reason: 'env-name', notes };
|
|
35
|
+
}
|
|
36
|
+
notes.push(`AIS_AGENT_NAME="${opts.envAgentName}" not found. Available: ${opts.agents.map((a) => a.name).join(', ')}`);
|
|
37
|
+
}
|
|
38
|
+
if (opts.preferredAgentId) {
|
|
39
|
+
const match = opts.agents.find((a) => a.id === opts.preferredAgentId);
|
|
40
|
+
if (match) {
|
|
41
|
+
return { agentId: match.id, reason: 'preferred', notes };
|
|
42
|
+
}
|
|
43
|
+
notes.push(`Previously bonded agent ${opts.preferredAgentId} is no longer active — switching to "${opts.agents[0].name}" (${opts.agents[0].id}).`);
|
|
44
|
+
}
|
|
45
|
+
return { agentId: opts.agents[0].id, reason: 'first-active', notes };
|
|
46
|
+
}
|
|
14
47
|
//# sourceMappingURL=env-agent.js.map
|
package/dist/env-agent.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env-agent.js","sourceRoot":"","sources":["../src/env-agent.ts"],"names":[],"mappings":"AAMA;;;GAGG;AACH,MAAM,UAAU,sBAAsB,CACpC,UAAkB,EAClB,MAA4B;IAE5B,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,UAAU,CAAC,CAAC;IAC9D,IAAI,KAAK,EAAE,CAAC;QACV,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAClF,MAAM,IAAI,KAAK,CACb,iBAAiB,UAAU,yCAAyC;QAClE,cAAc,SAAS,IAAI,QAAQ,EAAE,CACxC,CAAC;AACJ,CAAC"}
|
|
1
|
+
{"version":3,"file":"env-agent.js","sourceRoot":"","sources":["../src/env-agent.ts"],"names":[],"mappings":"AAMA;;;GAGG;AACH,MAAM,UAAU,sBAAsB,CACpC,UAAkB,EAClB,MAA4B;IAE5B,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,UAAU,CAAC,CAAC;IAC9D,IAAI,KAAK,EAAE,CAAC;QACV,OAAO,KAAK,CAAC;IACf,CAAC;IAED,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAClF,MAAM,IAAI,KAAK,CACb,iBAAiB,UAAU,yCAAyC;QAClE,cAAc,SAAS,IAAI,QAAQ,EAAE,CACxC,CAAC;AACJ,CAAC;AASD;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAQrC;IACC,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC7B,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC;IACtE,CAAC;IAED,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;QACtB,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,IAAI,CAAC,YAAa,CAAC,WAAW,EAAE,CAAC,CAAC;QACjG,IAAI,KAAK,EAAE,CAAC;YACV,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC;QAC1D,CAAC;QACD,KAAK,CAAC,IAAI,CACR,mBAAmB,IAAI,CAAC,YAAY,2BAA2B,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAC3G,CAAC;IACJ,CAAC;IAED,IAAI,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAC1B,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC,gBAAgB,CAAC,CAAC;QACtE,IAAI,KAAK,EAAE,CAAC;YACV,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC;QAC3D,CAAC;QACD,KAAK,CAAC,IAAI,CACR,2BAA2B,IAAI,CAAC,gBAAgB,wCAAwC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,IAAI,MAAM,IAAI,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,EAAE,IAAI,CACzI,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAE,CAAC,EAAE,EAAE,MAAM,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC;AACxE,CAAC"}
|