@agent-link/agent 0.1.272 → 0.1.274

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.
@@ -3,6 +3,7 @@ import os from 'os';
3
3
  import path from 'path';
4
4
  import { performance } from 'perf_hooks';
5
5
  import { createRequire } from 'module';
6
+ import { spawn } from 'child_process';
6
7
  import { loadRuntimeState, saveRuntimeState, getBrainDataDir, getConversationPermission, setConversationPermission, clearConversationPermission, getOrCreateE2EKey, encodeBase64Url, CONFIG_DIR } from './config.js';
7
8
  import { handleListDirectory, handleReadFile, handleUpdateFile, handleCreateFile, handleCreateDirectory, handleDeleteFile, handleUploadFile, handleUploadFileChunk, handleDownloadFile, handleDownloadChunkAck } from './directory-handlers.js';
8
9
  import { handleGitStatus, handleGitDiff, handleGitStage, handleGitUnstage, handleGitDiscard, handleGitCommit, handleGitWorktreeList, handleGitStash, handleGitPull, handleGitPush, handleGitDeleteFile, resolveToRepoRoot } from './git-handlers.js';
@@ -11,6 +12,8 @@ import { handleCreateLoop, handleUpdateLoop, handleDeleteLoop, handleListLoops,
11
12
  import { loadSessionMetadata, loadAllSessionMetadata, deleteSessionMetadata } from './session-metadata.js';
12
13
  import { listRecaps, listRecapsWithStatus, getRecapDetail, runActionItemsCli } from './recap.js';
13
14
  import { listActions, getActionDetail } from './action.js';
15
+ import { bindActionDraftSession, commitActionDraft, createActionDraft, discardActionDraft, getActionDraft, listActionDrafts, validateActionDraftId, } from './action-drafts.js';
16
+ import { resolveNewActionDraftSessionDir } from './action-draft-session.js';
14
17
  import { listIcms } from './playground-icm.js';
15
18
  import { listBriefings, getBriefingDetail } from './briefing.js';
16
19
  import { listDevops, getDevopsDetail } from './devops.js';
@@ -136,7 +139,7 @@ export function connect(config) {
136
139
  }
137
140
  catch { /* ignore */ }
138
141
  }
139
- unsubscribeSessionStarted = addOnSessionStarted(checkPendingActionItem);
142
+ unsubscribeSessionStarted = addOnSessionStarted(checkPendingSessionLinks);
140
143
  // Initialize the tunnel handler for port proxy
141
144
  tunnelHandler = createTunnelHandler(send);
142
145
  // Initialize the terminal manager for web terminal
@@ -144,7 +147,7 @@ export function connect(config) {
144
147
  // Wire up the Team module with send and claude dependencies
145
148
  setTeamSendFn(send);
146
149
  setTeamClaudeFns({
147
- handleChat: claudeHandleChat,
150
+ handleChat: guardedClaudeHandleChat,
148
151
  cancelExecution: claudeCancelExecution,
149
152
  addOutputObserver,
150
153
  removeOutputObserver,
@@ -159,7 +162,7 @@ export function connect(config) {
159
162
  // Initialize the Loop scheduler
160
163
  initScheduler({
161
164
  send,
162
- handleChat: claudeHandleChat,
165
+ handleChat: guardedClaudeHandleChat,
163
166
  cancelExecution: claudeCancelExecution,
164
167
  addOutputObserver,
165
168
  removeOutputObserver,
@@ -514,10 +517,120 @@ function scheduleReconnect(config) {
514
517
  }, delay);
515
518
  }
516
519
  const BRAIN_DATA_DIR = getBrainDataDir();
520
+ const FOLLOW_THROUGH_SESSION_DIR = path.join(BRAIN_DATA_DIR, 'reports', 'follow-through');
517
521
  const BRAIN_SERVER = 'http://localhost:8001';
518
522
  // Pending action item IDs: conversationId → actionItemId
519
523
  // Used to link a chat's Claude session to an action item for CLI start
520
524
  const pendingActionItems = new Map();
525
+ const pendingActionDrafts = new Map();
526
+ let agentUpgradeLaunchStarted = false;
527
+ let agentUpgradePending = false;
528
+ let agentUpgradeRecoveryTimer = null;
529
+ const AGENT_UPGRADE_RECOVERY_TIMEOUT_MS = 10 * 60 * 1000;
530
+ function guardedClaudeHandleChat(...args) {
531
+ if (agentUpgradePending) {
532
+ throw new Error('Agent upgrade is in progress. Please wait for the agent to reconnect.');
533
+ }
534
+ return claudeHandleChat(...args);
535
+ }
536
+ function collectActiveUpgradeSessions() {
537
+ const active = [];
538
+ const pushActive = (conversationId, conv) => {
539
+ if (!conv.turnActive)
540
+ return;
541
+ const sessionId = conv.claudeSessionId ?? conv.copilotSessionId ?? conv.codexSessionId ?? null;
542
+ active.push({
543
+ conversationId,
544
+ sessionId,
545
+ title: conv.sessionTitle || sessionId || conversationId,
546
+ });
547
+ };
548
+ for (const [convId, conv] of getConversations()) {
549
+ pushActive(convId, conv);
550
+ }
551
+ for (const [convId, conv] of getCopilotConversations()) {
552
+ pushActive(convId, conv);
553
+ }
554
+ for (const [convId, conv] of getCodexConversations()) {
555
+ pushActive(convId, conv);
556
+ }
557
+ for (const [executionId] of getRunningExecutions()) {
558
+ active.push({
559
+ conversationId: `loop:${executionId}`,
560
+ sessionId: executionId,
561
+ title: `Loop execution ${executionId.slice(0, 8)}`,
562
+ });
563
+ }
564
+ const activeTeam = getActiveTeam();
565
+ if (activeTeam && ['planning', 'running', 'summarizing'].includes(activeTeam.status)) {
566
+ active.push({
567
+ conversationId: activeTeam.conversationId,
568
+ sessionId: activeTeam.teamId,
569
+ title: activeTeam.title || `Team ${activeTeam.teamId.slice(0, 8)}`,
570
+ });
571
+ }
572
+ return active;
573
+ }
574
+ function launchDetachedAgentUpgrade(send) {
575
+ if (agentUpgradeLaunchStarted)
576
+ return;
577
+ const active = collectActiveUpgradeSessions();
578
+ if (active.length > 0) {
579
+ agentUpgradePending = false;
580
+ send({
581
+ type: 'agent_upgrade_blocked',
582
+ reason: 'active_conversations',
583
+ activeConversations: active,
584
+ });
585
+ return;
586
+ }
587
+ agentUpgradeLaunchStarted = true;
588
+ const env = { ...process.env };
589
+ delete env.AGENTLINK_PID;
590
+ try {
591
+ const child = process.platform === 'win32'
592
+ ? spawn('cmd.exe', ['/d', '/s', '/c', 'start "" /min agentlink-client upgrade'], {
593
+ detached: true,
594
+ stdio: 'ignore',
595
+ windowsHide: true,
596
+ env,
597
+ })
598
+ : spawn('sh', ['-c', 'agentlink-client upgrade >/dev/null 2>&1 &'], {
599
+ detached: true,
600
+ stdio: 'ignore',
601
+ env,
602
+ });
603
+ child.once('error', (error) => {
604
+ agentUpgradeLaunchStarted = false;
605
+ agentUpgradePending = false;
606
+ if (agentUpgradeRecoveryTimer) {
607
+ clearTimeout(agentUpgradeRecoveryTimer);
608
+ agentUpgradeRecoveryTimer = null;
609
+ }
610
+ send({ type: 'agent_upgrade_failed', message: `Failed to start upgrade: ${error.message}` });
611
+ });
612
+ child.unref();
613
+ agentUpgradeRecoveryTimer = setTimeout(() => {
614
+ agentUpgradeLaunchStarted = false;
615
+ agentUpgradePending = false;
616
+ agentUpgradeRecoveryTimer = null;
617
+ send({
618
+ type: 'agent_upgrade_failed',
619
+ message: 'Agent upgrade did not restart the agent. Run the upgrade command manually if this repeats.',
620
+ });
621
+ }, AGENT_UPGRADE_RECOVERY_TIMEOUT_MS);
622
+ agentUpgradeRecoveryTimer.unref?.();
623
+ }
624
+ catch (error) {
625
+ agentUpgradeLaunchStarted = false;
626
+ agentUpgradePending = false;
627
+ if (agentUpgradeRecoveryTimer) {
628
+ clearTimeout(agentUpgradeRecoveryTimer);
629
+ agentUpgradeRecoveryTimer = null;
630
+ }
631
+ send({ type: 'agent_upgrade_failed', message: `Failed to start upgrade: ${error.message}` });
632
+ }
633
+ }
521
634
  function handleServerMessage(msg) {
522
635
  console.log(`[AgentLink] ← ${msg.type}`);
523
636
  switch (msg.type) {
@@ -544,6 +657,10 @@ function handleServerMessage(msg) {
544
657
  break;
545
658
  }
546
659
  case 'chat': {
660
+ if (agentUpgradePending) {
661
+ send({ type: 'error', message: 'Agent upgrade is in progress. Please wait for the agent to reconnect.', conversationId: msg.conversationId });
662
+ break;
663
+ }
547
664
  const chatConvId = msg.conversationId;
548
665
  const existingConv = chatConvId ? getConversation(chatConvId) : getConversation();
549
666
  const isBrainMode = msg.brainMode;
@@ -556,9 +673,21 @@ function handleServerMessage(msg) {
556
673
  const projectName = msg.projectName;
557
674
  const icmId = msg.icmId;
558
675
  const actionItemId = msg.actionItemId;
676
+ const actionDraftId = msg.actionDraftId;
559
677
  const explicitWorkDir = msg.workDir;
678
+ if (actionDraftId) {
679
+ try {
680
+ validateActionDraftId(actionDraftId);
681
+ }
682
+ catch (err) {
683
+ send({ type: 'error', message: String(err), conversationId: chatConvId });
684
+ break;
685
+ }
686
+ }
560
687
  // Backward compat: brainMode: true without providerId → "brain"
561
- const effectiveProviderId = msgProviderId || (isBrainMode ? 'brain' : undefined);
688
+ const effectiveProviderId = actionDraftId
689
+ ? 'brain'
690
+ : (msgProviderId || (isBrainMode ? 'brain' : undefined));
562
691
  console.log(`[AgentLink] chat: conversationId=${chatConvId}, existingConv.planMode=${existingConv?.planMode}, providerId=${effectiveProviderId || '(default)'}`);
563
692
  const chatOptions = {
564
693
  resumeSessionId: msg.resumeSessionId,
@@ -587,13 +716,19 @@ function handleServerMessage(msg) {
587
716
  if (icmId) {
588
717
  chatOptions.icmId = String(icmId);
589
718
  }
719
+ if (actionDraftId) {
720
+ chatOptions.actionDraftId = actionDraftId;
721
+ }
590
722
  // Determine the working directory for this chat. For resumed sessions
591
723
  // the JSONL may live in a different cwd than the agent's current workDir
592
724
  // (e.g. action-item sessions spawned by BrainCore live in $HOME). When
593
725
  // no existingConv is found (agent restarted, or cache-hit skipped
594
726
  // resume_conversation), probe the filesystem to find the right directory.
595
- let chatDir = explicitWorkDir ?? existingConv?.workDir;
596
- if (!chatDir && (recapId || briefingDate || devopsEntityType || projectName || icmId)) {
727
+ const newActionDraftSessionDir = resolveNewActionDraftSessionDir(BRAIN_DATA_DIR, actionDraftId, explicitWorkDir, chatOptions.resumeSessionId, existingConv);
728
+ let chatDir = newActionDraftSessionDir ?? explicitWorkDir ?? existingConv?.workDir;
729
+ if (newActionDraftSessionDir)
730
+ mkdirSync(FOLLOW_THROUGH_SESSION_DIR, { recursive: true });
731
+ if (!chatDir && (recapId || briefingDate || devopsEntityType || projectName || icmId || actionDraftId)) {
597
732
  // Brain-mode session: try BRAIN_DATA_DIR first, then homedir fallback
598
733
  const probeSessionId = chatOptions.resumeSessionId
599
734
  || existingConv?.claudeSessionId;
@@ -617,6 +752,9 @@ function handleServerMessage(msg) {
617
752
  if (actionItemId && chatConvId) {
618
753
  pendingActionItems.set(chatConvId, actionItemId);
619
754
  }
755
+ if (actionDraftId && chatConvId) {
756
+ pendingActionDrafts.set(chatConvId, { draftId: actionDraftId, sessionCwd: chatDir });
757
+ }
620
758
  // Auto-switch backend if chat message's providerId requires a different backend
621
759
  if (effectiveProviderId) {
622
760
  const requiredBt = getBackendTypeForProvider(effectiveProviderId);
@@ -1170,6 +1308,21 @@ function handleServerMessage(msg) {
1170
1308
  });
1171
1309
  break;
1172
1310
  }
1311
+ case 'trigger_agent_upgrade': {
1312
+ const active = collectActiveUpgradeSessions();
1313
+ if (active.length > 0) {
1314
+ send({
1315
+ type: 'agent_upgrade_blocked',
1316
+ reason: 'active_conversations',
1317
+ activeConversations: active,
1318
+ });
1319
+ break;
1320
+ }
1321
+ agentUpgradePending = true;
1322
+ send({ type: 'agent_upgrade_started' });
1323
+ setTimeout(() => launchDetachedAgentUpgrade(send), 200);
1324
+ break;
1325
+ }
1173
1326
  case 'query_turn_status': {
1174
1327
  const m = msg;
1175
1328
  const buildTurnStatus = (convId, conv) => {
@@ -1245,6 +1398,10 @@ function handleServerMessage(msg) {
1245
1398
  break;
1246
1399
  }
1247
1400
  case 'create_team':
1401
+ if (agentUpgradePending) {
1402
+ send({ type: 'error', message: 'Agent upgrade is in progress. Please wait for the agent to reconnect.' });
1403
+ break;
1404
+ }
1248
1405
  mkdirSync(TEAM_SESSIONS_DIR, { recursive: true });
1249
1406
  handleCreateTeam(msg, TEAM_SESSIONS_DIR, send);
1250
1407
  break;
@@ -1284,6 +1441,10 @@ function handleServerMessage(msg) {
1284
1441
  handleGetLoop(msg, send);
1285
1442
  break;
1286
1443
  case 'run_loop':
1444
+ if (agentUpgradePending) {
1445
+ send({ type: 'error', message: 'Agent upgrade is in progress. Please wait for the agent to reconnect.' });
1446
+ break;
1447
+ }
1287
1448
  handleRunLoop(msg, send);
1288
1449
  break;
1289
1450
  case 'cancel_loop_execution':
@@ -1612,6 +1773,21 @@ function handleServerMessage(msg) {
1612
1773
  case 'complete_action_item':
1613
1774
  handleCompleteActionItem(msg);
1614
1775
  break;
1776
+ case 'create_action_draft':
1777
+ handleCreateActionDraft(msg);
1778
+ break;
1779
+ case 'list_action_drafts':
1780
+ handleListActionDrafts(msg);
1781
+ break;
1782
+ case 'get_action_draft':
1783
+ handleGetActionDraft(msg);
1784
+ break;
1785
+ case 'commit_action_draft':
1786
+ handleCommitActionDraft(msg);
1787
+ break;
1788
+ case 'discard_action_draft':
1789
+ handleDiscardActionDraft(msg);
1790
+ break;
1615
1791
  case 'list_briefings':
1616
1792
  handleListBriefings();
1617
1793
  break;
@@ -2381,6 +2557,78 @@ async function handleCompleteActionItem(msg) {
2381
2557
  send({ type: 'action_item_completed', actionItemId: msg.actionItemId, error: String(err) });
2382
2558
  }
2383
2559
  }
2560
+ async function handleCreateActionDraft(msg) {
2561
+ try {
2562
+ const draft = await createActionDraft();
2563
+ send({ type: 'action_draft_created', requestId: msg.requestId, draft });
2564
+ }
2565
+ catch (err) {
2566
+ console.error('[AgentLink] createActionDraft failed:', err);
2567
+ send({
2568
+ type: 'action_draft_created',
2569
+ requestId: msg.requestId,
2570
+ draft: null,
2571
+ error: String(err),
2572
+ });
2573
+ }
2574
+ }
2575
+ async function handleListActionDrafts(msg) {
2576
+ try {
2577
+ const drafts = await listActionDrafts(msg.status);
2578
+ send({ type: 'action_drafts_list', drafts });
2579
+ }
2580
+ catch (err) {
2581
+ console.error('[AgentLink] listActionDrafts failed:', err);
2582
+ send({ type: 'action_drafts_list', drafts: [], error: String(err) });
2583
+ }
2584
+ }
2585
+ async function handleGetActionDraft(msg) {
2586
+ try {
2587
+ const draft = await getActionDraft(msg.draftId);
2588
+ send({ type: 'action_draft', draftId: msg.draftId, draft });
2589
+ }
2590
+ catch (err) {
2591
+ console.error(`[AgentLink] getActionDraft failed for ${msg.draftId}:`, err);
2592
+ send({ type: 'action_draft', draftId: msg.draftId, draft: null, error: String(err) });
2593
+ }
2594
+ }
2595
+ async function handleCommitActionDraft(msg) {
2596
+ try {
2597
+ const result = await commitActionDraft(msg.draftId, msg.expectedRevision);
2598
+ const actionItemId = String(result.action.action_item_id || result.draft.action_item_id || '');
2599
+ send({
2600
+ type: 'action_draft_committed',
2601
+ draftId: msg.draftId,
2602
+ actionItemId,
2603
+ draft: result.draft,
2604
+ action: result.action,
2605
+ });
2606
+ }
2607
+ catch (err) {
2608
+ console.error(`[AgentLink] commitActionDraft failed for ${msg.draftId}:`, err);
2609
+ send({
2610
+ type: 'action_draft_committed',
2611
+ draftId: msg.draftId,
2612
+ actionItemId: null,
2613
+ error: String(err),
2614
+ });
2615
+ }
2616
+ }
2617
+ async function handleDiscardActionDraft(msg) {
2618
+ try {
2619
+ const draft = await discardActionDraft(msg.draftId);
2620
+ send({ type: 'action_draft_discarded', draftId: msg.draftId, draft });
2621
+ }
2622
+ catch (err) {
2623
+ console.error(`[AgentLink] discardActionDraft failed for ${msg.draftId}:`, err);
2624
+ send({
2625
+ type: 'action_draft_discarded',
2626
+ draftId: msg.draftId,
2627
+ draft: null,
2628
+ error: String(err),
2629
+ });
2630
+ }
2631
+ }
2384
2632
  /**
2385
2633
  * Called when a recap chat session starts to check if it's linked to an action item.
2386
2634
  * If so, calls CLI `start` to transition the action item to 'running' status.
@@ -2400,4 +2648,35 @@ export function checkPendingActionItem(conversationId, claudeSessionId) {
2400
2648
  send({ type: 'action_item_started', actionItemId, sessionId: claudeSessionId, error: String(err) });
2401
2649
  });
2402
2650
  }
2651
+ function checkPendingActionDraft(conversationId, claudeSessionId) {
2652
+ const pending = pendingActionDrafts.get(conversationId);
2653
+ if (!pending)
2654
+ return;
2655
+ pendingActionDrafts.delete(conversationId);
2656
+ const bind = (attempt) => {
2657
+ console.log(`[AgentLink] Binding action draft ${pending.draftId} to session ${claudeSessionId} (attempt ${attempt + 1})`);
2658
+ bindActionDraftSession(pending.draftId, claudeSessionId, pending.sessionCwd)
2659
+ .then((draft) => {
2660
+ send({ type: 'action_draft', draftId: pending.draftId, draft });
2661
+ })
2662
+ .catch((err) => {
2663
+ if (attempt < 2) {
2664
+ setTimeout(() => bind(attempt + 1), 500 * (attempt + 1));
2665
+ return;
2666
+ }
2667
+ console.error('[AgentLink] bind action draft session failed:', err);
2668
+ send({
2669
+ type: 'action_draft',
2670
+ draftId: pending.draftId,
2671
+ draft: null,
2672
+ error: String(err),
2673
+ });
2674
+ });
2675
+ };
2676
+ bind(0);
2677
+ }
2678
+ export function checkPendingSessionLinks(conversationId, claudeSessionId) {
2679
+ checkPendingActionItem(conversationId, claudeSessionId);
2680
+ checkPendingActionDraft(conversationId, claudeSessionId);
2681
+ }
2403
2682
  //# sourceMappingURL=connection.js.map