@agent-link/agent 0.1.289 → 0.1.292
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/action.d.ts +7 -0
- package/dist/action.js.map +1 -1
- package/dist/cli.js +137 -25
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +9 -0
- package/dist/config.js +25 -3
- package/dist/config.js.map +1 -1
- package/dist/connection.d.ts +12 -0
- package/dist/connection.js +145 -4
- package/dist/connection.js.map +1 -1
- package/dist/encryption.d.ts +4 -0
- package/dist/encryption.js +13 -0
- package/dist/encryption.js.map +1 -1
- package/dist/index.js +54 -10
- package/dist/index.js.map +1 -1
- package/dist/local-control-client.d.ts +15 -0
- package/dist/local-control-client.js +100 -0
- package/dist/local-control-client.js.map +1 -0
- package/dist/local-control-http.d.ts +7 -0
- package/dist/local-control-http.js +208 -0
- package/dist/local-control-http.js.map +1 -0
- package/dist/local-control.d.ts +74 -0
- package/dist/local-control.js +55 -0
- package/dist/local-control.js.map +1 -0
- package/dist/process-control.d.ts +17 -0
- package/dist/process-control.js +48 -0
- package/dist/process-control.js.map +1 -0
- package/dist/shutdown-coordinator.d.ts +19 -0
- package/dist/shutdown-coordinator.js +67 -0
- package/dist/shutdown-coordinator.js.map +1 -0
- package/dist/upgrade-job.d.ts +25 -0
- package/dist/upgrade-job.js +59 -0
- package/dist/upgrade-job.js.map +1 -0
- package/package.json +1 -1
package/dist/connection.js
CHANGED
|
@@ -22,7 +22,7 @@ import { listBrainSessions, loadBrainChatEntries, sendBrainChatMessage, getBrain
|
|
|
22
22
|
import { listProviders, saveProvider, deleteProvider, getDefaultProviderId, setDefaultProviderId, getBackendTypeForProvider } from './providers.js';
|
|
23
23
|
import { createTunnelHandler } from './tunnel.js';
|
|
24
24
|
import { createTerminalManager } from './terminal.js';
|
|
25
|
-
import { existsSync, mkdirSync } from 'fs';
|
|
25
|
+
import { existsSync, mkdirSync, readdirSync, statSync } from 'fs';
|
|
26
26
|
const require = createRequire(import.meta.url);
|
|
27
27
|
const pkg = require('../package.json');
|
|
28
28
|
const TEAM_SESSIONS_DIR = path.join(CONFIG_DIR, 'team-sessions');
|
|
@@ -33,7 +33,7 @@ import { claudeCapabilities } from './backends/types.js';
|
|
|
33
33
|
import { searchSessions } from './search-sessions.js';
|
|
34
34
|
import { formatSessionExport, isSessionExportFormat } from './session-export.js';
|
|
35
35
|
import { listMemoryFiles, updateMemoryFile, deleteMemoryFile } from './memory.js';
|
|
36
|
-
import { decodeKey, parseMessage, encryptAndSend, e2eEncrypt, e2eDecrypt, isE2EEncrypted } from './encryption.js';
|
|
36
|
+
import { decodeKey, parseMessage, encryptAndSend, encryptAndSendConfirmed, e2eEncrypt, e2eDecrypt, isE2EEncrypted } from './encryption.js';
|
|
37
37
|
import { setTeamSendFn, setTeamClaudeFns, getActiveTeam, serializeTeam } from './team.js';
|
|
38
38
|
import { refreshEntraToken, isTokenExpired } from './entra.js';
|
|
39
39
|
import { initEntraTimers, startEntraRetryTimer, stopEntraRetryTimer, scheduleEntraRefresh, cancelEntraRefresh, } from './agent-entra-timer.js';
|
|
@@ -57,6 +57,8 @@ const state = {
|
|
|
57
57
|
workDir: process.cwd(),
|
|
58
58
|
config: null,
|
|
59
59
|
entraToken: null,
|
|
60
|
+
acceptingWork: true,
|
|
61
|
+
relayReady: false,
|
|
60
62
|
};
|
|
61
63
|
let unsubscribeSend = null;
|
|
62
64
|
let unsubscribeCopilotSend = null;
|
|
@@ -147,6 +149,8 @@ export function connect(config) {
|
|
|
147
149
|
state.workDir = config.dir;
|
|
148
150
|
state.config = config;
|
|
149
151
|
state.shouldReconnect = true;
|
|
152
|
+
state.acceptingWork = true;
|
|
153
|
+
state.relayReady = false;
|
|
150
154
|
setBackendType(config.agent ?? 'claude');
|
|
151
155
|
// Initialize E2E encryption key
|
|
152
156
|
const ignoreConfig = !!process.env.AGENTLINK_NO_STATE;
|
|
@@ -309,6 +313,7 @@ function doConnect(config, onRegistered, onError) {
|
|
|
309
313
|
settled = true;
|
|
310
314
|
onRegistered(state.sessionId);
|
|
311
315
|
}
|
|
316
|
+
state.relayReady = state.config?.entra !== true;
|
|
312
317
|
// Always check for session ID changes and update runtime state
|
|
313
318
|
if (previousSessionId && previousSessionId !== newSessionId) {
|
|
314
319
|
console.warn(`[AgentLink] Session ID changed: ${previousSessionId} → ${newSessionId}`);
|
|
@@ -421,6 +426,7 @@ function doConnect(config, onRegistered, onError) {
|
|
|
421
426
|
}
|
|
422
427
|
});
|
|
423
428
|
ws.on('close', () => {
|
|
429
|
+
state.relayReady = false;
|
|
424
430
|
stopHeartbeat();
|
|
425
431
|
console.log('[AgentLink] Disconnected from server');
|
|
426
432
|
if (state.shouldReconnect) {
|
|
@@ -518,6 +524,46 @@ export function send(msg) {
|
|
|
518
524
|
console.warn(`[AgentLink] Cannot send ${msg.type}: WebSocket not open (readyState=${state.ws?.readyState})`);
|
|
519
525
|
}
|
|
520
526
|
}
|
|
527
|
+
export async function sendAndFlush(msg, timeoutMs = 1500) {
|
|
528
|
+
await sendQueue;
|
|
529
|
+
const ws = state.ws;
|
|
530
|
+
if (!ws || ws.readyState !== WebSocket.OPEN)
|
|
531
|
+
return;
|
|
532
|
+
let payload = msg;
|
|
533
|
+
if (state.e2eKey && state.e2eNegotiated) {
|
|
534
|
+
payload = await e2eEncrypt(msg, state.e2eKey);
|
|
535
|
+
}
|
|
536
|
+
let timeout;
|
|
537
|
+
try {
|
|
538
|
+
await Promise.race([
|
|
539
|
+
encryptAndSendConfirmed(ws, payload, state.sessionKey),
|
|
540
|
+
new Promise((_, reject) => {
|
|
541
|
+
timeout = setTimeout(() => reject(new Error(`WebSocket send did not drain within ${timeoutMs}ms`)), timeoutMs);
|
|
542
|
+
timeout.unref();
|
|
543
|
+
}),
|
|
544
|
+
]);
|
|
545
|
+
}
|
|
546
|
+
finally {
|
|
547
|
+
if (timeout)
|
|
548
|
+
clearTimeout(timeout);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
export function beginShutdown() {
|
|
552
|
+
state.acceptingWork = false;
|
|
553
|
+
}
|
|
554
|
+
export function getConnectionStatus() {
|
|
555
|
+
return {
|
|
556
|
+
websocketConnected: state.ws?.readyState === WebSocket.OPEN,
|
|
557
|
+
relayReady: state.relayReady,
|
|
558
|
+
acceptingWork: state.acceptingWork,
|
|
559
|
+
sessionId: state.sessionId,
|
|
560
|
+
workDir: state.workDir,
|
|
561
|
+
backend: state.config?.agent ?? 'claude',
|
|
562
|
+
conversations: getConversations().size
|
|
563
|
+
+ getCopilotConversations().size
|
|
564
|
+
+ getCodexConversations().size,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
521
567
|
function buildWsUrl(config) {
|
|
522
568
|
const base = config.server.replace(/\/$/, '');
|
|
523
569
|
const params = new URLSearchParams({
|
|
@@ -576,13 +622,35 @@ const pendingActionItems = new Map();
|
|
|
576
622
|
const pendingActionDrafts = new Map();
|
|
577
623
|
function handleServerMessage(msg) {
|
|
578
624
|
console.log(`[AgentLink] ← ${msg.type}`);
|
|
625
|
+
const shutdownAllowedMessages = new Set([
|
|
626
|
+
'ping',
|
|
627
|
+
'pong',
|
|
628
|
+
'entra_auth_ok',
|
|
629
|
+
'entra_auth_failed',
|
|
630
|
+
'cancel_execution',
|
|
631
|
+
'terminal_close',
|
|
632
|
+
'tunnel_ws_close',
|
|
633
|
+
]);
|
|
634
|
+
if (!state.acceptingWork && !shutdownAllowedMessages.has(msg.type)) {
|
|
635
|
+
send({
|
|
636
|
+
type: 'error',
|
|
637
|
+
code: 'agent_shutting_down',
|
|
638
|
+
message: 'Agent is shutting down and is not accepting new work.',
|
|
639
|
+
conversationId: msg.conversationId,
|
|
640
|
+
requestId: msg.requestId,
|
|
641
|
+
rejectedType: msg.type,
|
|
642
|
+
});
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
579
645
|
switch (msg.type) {
|
|
580
646
|
case 'entra_auth_ok':
|
|
647
|
+
state.relayReady = true;
|
|
581
648
|
console.log(`[AgentLink] Entra identity verified: ${msg.email}`);
|
|
582
649
|
stopEntraRetryTimer();
|
|
583
650
|
scheduleEntraRefresh();
|
|
584
651
|
break;
|
|
585
652
|
case 'entra_auth_failed': {
|
|
653
|
+
state.relayReady = false;
|
|
586
654
|
const reason = msg.reason;
|
|
587
655
|
console.error(`[AgentLink] Entra auth failed: ${reason}`);
|
|
588
656
|
if (reason === 'token_expired') {
|
|
@@ -1610,9 +1678,13 @@ function handleServerMessage(msg) {
|
|
|
1610
1678
|
}
|
|
1611
1679
|
}
|
|
1612
1680
|
else {
|
|
1613
|
-
// Same backend (claude)
|
|
1681
|
+
// Same backend (claude): reconnect provider sync must not interrupt
|
|
1682
|
+
// an active Claude process.
|
|
1614
1683
|
const conv = getConversation(conversationId);
|
|
1615
|
-
if (conv) {
|
|
1684
|
+
if (conv?.turnActive) {
|
|
1685
|
+
conv.providerId = providerId;
|
|
1686
|
+
}
|
|
1687
|
+
else if (conv) {
|
|
1616
1688
|
const result = restartConversation(conversationId, { providerId });
|
|
1617
1689
|
if (result.wasTurnActive) {
|
|
1618
1690
|
send({ type: 'execution_cancelled', conversationId });
|
|
@@ -1758,6 +1830,9 @@ function handleServerMessage(msg) {
|
|
|
1758
1830
|
case 'retry_action_item':
|
|
1759
1831
|
handleRetryActionItem(msg);
|
|
1760
1832
|
break;
|
|
1833
|
+
case 'reopen_action_item':
|
|
1834
|
+
handleReopenActionItem(msg);
|
|
1835
|
+
break;
|
|
1761
1836
|
case 'create_action_draft':
|
|
1762
1837
|
handleCreateActionDraft(msg);
|
|
1763
1838
|
break;
|
|
@@ -2495,6 +2570,41 @@ async function handleDismissActionItem(msg) {
|
|
|
2495
2570
|
}
|
|
2496
2571
|
}
|
|
2497
2572
|
// ── Actions Feed handlers ──────────────────────────────────────────────
|
|
2573
|
+
// Resolve the on-disk artifact reference for an action item, if any. All
|
|
2574
|
+
// follow-through items write their deliverables to
|
|
2575
|
+
// {BRAIN_DATA_DIR}/reports/follow-through/artifacts/{action_item_id}/
|
|
2576
|
+
// We surface it as a structured field so the card can show a "View artifact"
|
|
2577
|
+
// link without the frontend having to parse file paths out of summary prose.
|
|
2578
|
+
// Returns { artifact_path, artifact_label, artifact_is_dir } or {} when none.
|
|
2579
|
+
function resolveArtifact(actionItemId) {
|
|
2580
|
+
try {
|
|
2581
|
+
if (!actionItemId)
|
|
2582
|
+
return {};
|
|
2583
|
+
const dir = path.join(BRAIN_DATA_DIR, 'reports', 'follow-through', 'artifacts', actionItemId);
|
|
2584
|
+
if (!existsSync(dir))
|
|
2585
|
+
return {};
|
|
2586
|
+
const entries = readdirSync(dir).filter(f => !f.startsWith('.'));
|
|
2587
|
+
if (entries.length === 0)
|
|
2588
|
+
return {};
|
|
2589
|
+
// Prefer a single primary document: if exactly one file, link straight to
|
|
2590
|
+
// it; otherwise link to the directory and let the user pick.
|
|
2591
|
+
const files = entries.filter(f => {
|
|
2592
|
+
try {
|
|
2593
|
+
return statSync(path.join(dir, f)).isFile();
|
|
2594
|
+
}
|
|
2595
|
+
catch {
|
|
2596
|
+
return false;
|
|
2597
|
+
}
|
|
2598
|
+
});
|
|
2599
|
+
if (files.length === 1) {
|
|
2600
|
+
return { artifact_path: path.join(dir, files[0]), artifact_label: files[0], artifact_is_dir: false };
|
|
2601
|
+
}
|
|
2602
|
+
return { artifact_path: dir, artifact_label: `${files.length || entries.length} files`, artifact_is_dir: true };
|
|
2603
|
+
}
|
|
2604
|
+
catch {
|
|
2605
|
+
return {};
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2498
2608
|
async function handleListActions() {
|
|
2499
2609
|
try {
|
|
2500
2610
|
const actions = await listActions();
|
|
@@ -2508,6 +2618,7 @@ async function handleListActions() {
|
|
|
2508
2618
|
...a,
|
|
2509
2619
|
meeting_name: a.meeting_name ?? (a.recap_id ? meetingNameByRecapId.get(a.recap_id) : undefined),
|
|
2510
2620
|
work_dir: a.work_dir ?? a.session_cwd ?? homeDir,
|
|
2621
|
+
...resolveArtifact(a.action_item_id),
|
|
2511
2622
|
}));
|
|
2512
2623
|
console.log(`[AgentLink] → actions_list (${actionsWithWorkDir.length} items)`);
|
|
2513
2624
|
send({ type: 'actions_list', actions: actionsWithWorkDir });
|
|
@@ -2571,6 +2682,36 @@ async function handleRetryActionItem(msg) {
|
|
|
2571
2682
|
clearTimeout(timer);
|
|
2572
2683
|
}
|
|
2573
2684
|
}
|
|
2685
|
+
async function handleReopenActionItem(msg) {
|
|
2686
|
+
// Reopen reactivates a terminal item (completed/dropped -> ready) and must
|
|
2687
|
+
// re-index it in the dispatcher, so POST to BrainServer's REST endpoint
|
|
2688
|
+
// rather than a bare CLI set-status.
|
|
2689
|
+
const controller = new AbortController();
|
|
2690
|
+
const timer = setTimeout(() => controller.abort(), 15000);
|
|
2691
|
+
try {
|
|
2692
|
+
const res = await fetch(`${BRAIN_SERVER}/api/actions/${encodeURIComponent(msg.actionItemId)}/reopen`, {
|
|
2693
|
+
method: 'POST',
|
|
2694
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2695
|
+
body: JSON.stringify(msg.reopenedFrom ? { reopened_from: msg.reopenedFrom } : {}),
|
|
2696
|
+
signal: controller.signal,
|
|
2697
|
+
});
|
|
2698
|
+
if (!res.ok) {
|
|
2699
|
+
// Forward the HTTP status so the client can treat a 409 (item already
|
|
2700
|
+
// reopened / no longer terminal) as "already reopened", not an error.
|
|
2701
|
+
send({ type: 'action_item_reopened', actionItemId: msg.actionItemId, status: res.status, error: `reopen failed: HTTP ${res.status}` });
|
|
2702
|
+
return;
|
|
2703
|
+
}
|
|
2704
|
+
console.log(`[AgentLink] → action_item_reopened (${msg.actionItemId})`);
|
|
2705
|
+
send({ type: 'action_item_reopened', actionItemId: msg.actionItemId });
|
|
2706
|
+
}
|
|
2707
|
+
catch (err) {
|
|
2708
|
+
console.error(`[AgentLink] reopenActionItem failed for ${msg.actionItemId}:`, err);
|
|
2709
|
+
send({ type: 'action_item_reopened', actionItemId: msg.actionItemId, error: String(err) });
|
|
2710
|
+
}
|
|
2711
|
+
finally {
|
|
2712
|
+
clearTimeout(timer);
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2574
2715
|
async function handleCreateActionDraft(msg) {
|
|
2575
2716
|
try {
|
|
2576
2717
|
const draft = await createActionDraft();
|