@aion0/forge 0.5.12 → 0.5.14

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/RELEASE_NOTES.md CHANGED
@@ -1,13 +1,14 @@
1
- # Forge v0.5.12
1
+ # Forge v0.5.14
2
2
 
3
3
  Released: 2026-03-31
4
4
 
5
- ## Changes since v0.5.11
5
+ ## Changes since v0.5.13
6
6
 
7
7
  ### Bug Fixes
8
- - fix: correct bin paths in package.json
9
- - fix: merge pending upstream_complete auto-complete older ones before sending new
10
- - fix: upstream_complete notification tells agent to ignore if busy or duplicate
8
+ - fix: dedup forge failed notifications by sender→target pair
9
+ - fix: TerminalLaunchDialog also uses daemon to create session
10
+ - fix: FloatingTerminal only attaches, daemon creates all sessions
11
+ - fix: write launch script to file to avoid tmux send-keys truncation
11
12
 
12
13
 
13
- **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.11...v0.5.12
14
+ **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.13...v0.5.14
@@ -2517,43 +2517,20 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
2517
2517
  },
2518
2518
  };
2519
2519
 
2520
- // If tmux session exists attach (primary or non-primary)
2521
- if (existingTmux) {
2522
- wsApi(workspaceId, 'open_terminal', { agentId: agent.id });
2523
- setFloatingTerminals(prev => [...prev, {
2524
- agentId: agent.id, label: agent.label, icon: agent.icon,
2525
- cliId: agent.agentId || 'claude', ...launchInfo, workDir,
2526
- tmuxSession: existingTmux, sessionName: sessName,
2527
- isPrimary: agent.primary, skipPermissions: agent.skipPermissions !== false, persistentSession: agent.persistentSession, boundSessionId: agent.boundSessionId, initialPos,
2528
- }]);
2529
- return;
2530
- }
2531
-
2532
- // Primary without session → open directly (no dialog)
2533
- if (agent.primary) {
2534
- const res = await wsApi(workspaceId, 'open_terminal', { agentId: agent.id }).catch(() => ({})) as any;
2535
- setFloatingTerminals(prev => [...prev, {
2536
- agentId: agent.id, label: agent.label, icon: agent.icon,
2537
- cliId: agent.agentId || 'claude', ...launchInfo, workDir,
2538
- tmuxSession: res?.tmuxSession || sessName, sessionName: sessName,
2539
- isPrimary: true, skipPermissions: agent.skipPermissions !== false, persistentSession: agent.persistentSession, boundSessionId: agent.boundSessionId, initialPos,
2540
- }]);
2541
- return;
2542
- }
2543
-
2544
- // Non-primary: has boundSessionId → use it directly; no bound → show dialog
2545
- if (agent.boundSessionId) {
2520
+ // All paths: let daemon create/ensure session, then attach
2521
+ if (existingTmux || agent.primary || agent.persistentSession || agent.boundSessionId) {
2522
+ // Daemon creates session via ensurePersistentSession (launch script, no truncation)
2546
2523
  const res = await wsApi(workspaceId, 'open_terminal', { agentId: agent.id }).catch(() => ({})) as any;
2524
+ const tmux = existingTmux || res?.tmuxSession || sessName;
2547
2525
  setFloatingTerminals(prev => [...prev, {
2548
2526
  agentId: agent.id, label: agent.label, icon: agent.icon,
2549
- cliId: agent.agentId || 'claude', ...launchInfo, workDir,
2550
- tmuxSession: res?.tmuxSession || sessName, sessionName: sessName,
2551
- resumeSessionId: agent.boundSessionId,
2552
- isPrimary: false, skipPermissions: agent.skipPermissions !== false, persistentSession: agent.persistentSession, boundSessionId: agent.boundSessionId, initialPos,
2527
+ cliId: agent.agentId || 'claude', workDir,
2528
+ tmuxSession: tmux, sessionName: sessName,
2529
+ isPrimary: agent.primary, skipPermissions: agent.skipPermissions !== false, persistentSession: agent.persistentSession, boundSessionId: agent.boundSessionId, initialPos,
2553
2530
  }]);
2554
2531
  return;
2555
2532
  }
2556
- // No bound session → show launch dialog (New / Resume / Select)
2533
+ // No persistent session, no bound session → show launch dialog
2557
2534
  setTermLaunchDialog({ agent, sessName, workDir, sessions: [], supportsSession: resolveRes?.supportsSession ?? true, initialPos });
2558
2535
  },
2559
2536
  onSwitchSession: async () => {
@@ -2934,28 +2911,19 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
2934
2911
  onLaunch={async (resumeMode, sessionId) => {
2935
2912
  const { agent, sessName, workDir } = termLaunchDialog;
2936
2913
  setTermLaunchDialog(null);
2937
- const res = await wsApi(workspaceId, 'open_terminal', { agentId: agent.id });
2938
- if (res.ok) {
2939
- // Save selected session as boundSessionId if user chose a specific one
2940
- if (sessionId) {
2941
- wsApi(workspaceId, 'update', { agentId: agent.id, config: { ...agent, boundSessionId: sessionId } }).catch(() => {});
2942
- }
2943
- setFloatingTerminals(prev => [...prev, {
2944
- agentId: agent.id, label: agent.label, icon: agent.icon,
2945
- cliId: agent.agentId || 'claude',
2946
- cliCmd: res.cliCmd || 'claude',
2947
- cliType: res.cliType || 'claude-code',
2948
- workDir,
2949
- sessionName: sessName, resumeMode, resumeSessionId: sessionId, isPrimary: false, skipPermissions: agent.skipPermissions !== false, persistentSession: agent.persistentSession, boundSessionId: sessionId || agent.boundSessionId, initialPos: termLaunchDialog.initialPos,
2950
- profileEnv: {
2951
- ...(res.env || {}),
2952
- ...(res.model ? { CLAUDE_MODEL: res.model } : {}),
2953
- FORGE_AGENT_ID: agent.id,
2954
- FORGE_WORKSPACE_ID: workspaceId,
2955
- FORGE_PORT: String(window.location.port || 8403),
2956
- },
2957
- }]);
2914
+ // Save selected session as boundSessionId
2915
+ if (sessionId) {
2916
+ await wsApi(workspaceId, 'update', { agentId: agent.id, config: { ...agent, boundSessionId: sessionId } }).catch(() => {});
2958
2917
  }
2918
+ // Daemon creates session (launch script), then attach
2919
+ const res = await wsApi(workspaceId, 'open_terminal', { agentId: agent.id }).catch(() => ({})) as any;
2920
+ const tmux = res?.tmuxSession || sessName;
2921
+ setFloatingTerminals(prev => [...prev, {
2922
+ agentId: agent.id, label: agent.label, icon: agent.icon,
2923
+ cliId: agent.agentId || 'claude', workDir,
2924
+ tmuxSession: tmux, sessionName: sessName,
2925
+ isPrimary: false, skipPermissions: agent.skipPermissions !== false, persistentSession: agent.persistentSession, boundSessionId: sessionId || agent.boundSessionId, initialPos: termLaunchDialog.initialPos,
2926
+ }]);
2959
2927
  }}
2960
2928
  onCancel={() => setTermLaunchDialog(null)}
2961
2929
  />
@@ -1353,16 +1353,20 @@ export class WorkspaceOrchestrator extends EventEmitter {
1353
1353
  }
1354
1354
  }
1355
1355
 
1356
- // Case 4: Failed → notify sender so they know
1357
- if (msg.status === 'failed' && !this.forgeActedMessages.has(`failed-${msg.id}`)) {
1358
- const senderEntry = this.agents.get(msg.from);
1359
- const targetLabel = this.agents.get(msg.to)?.config.label || msg.to;
1360
- if (senderEntry && msg.from !== '_forge' && msg.from !== '_system') {
1361
- this.bus.send('_forge', msg.from, 'notify', {
1362
- action: 'update_notify',
1363
- content: `Your message to ${targetLabel} has failed. You may want to retry or take a different approach.`,
1364
- });
1365
- console.log(`[forge-agent] Notified ${senderEntry.config.label} that message to ${targetLabel} failed`);
1356
+ // Case 4: Failed → notify sender (once per sender→target pair)
1357
+ if (msg.status === 'failed') {
1358
+ const failKey = `failed-${msg.from}->${msg.to}`;
1359
+ if (!this.forgeActedMessages.has(failKey)) {
1360
+ const senderEntry = this.agents.get(msg.from);
1361
+ const targetLabel = this.agents.get(msg.to)?.config.label || msg.to;
1362
+ if (senderEntry && msg.from !== '_forge' && msg.from !== '_system') {
1363
+ this.bus.send('_forge', msg.from, 'notify', {
1364
+ action: 'update_notify',
1365
+ content: `Your message to ${targetLabel} has failed. You may want to retry or take a different approach.`,
1366
+ });
1367
+ console.log(`[forge-agent] Notified ${senderEntry.config.label} that message to ${targetLabel} failed (once)`);
1368
+ }
1369
+ this.forgeActedMessages.add(failKey);
1366
1370
  }
1367
1371
  this.forgeActedMessages.add(`failed-${msg.id}`);
1368
1372
  }
@@ -2023,27 +2027,24 @@ export class WorkspaceOrchestrator extends EventEmitter {
2023
2027
 
2024
2028
  execSync(`tmux new-session -d -s "${sessionName}" -c "${workDir}"`, { timeout: 5000 });
2025
2029
 
2026
- // Reset profile env vars (unset any leftover from previous agent) then set new ones
2027
- const profileVarsToReset = ['ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_BASE_URL', 'ANTHROPIC_SMALL_FAST_MODEL', 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC', 'DISABLE_TELEMETRY', 'DISABLE_ERROR_REPORTING', 'DISABLE_AUTOUPDATER', 'DISABLE_NON_ESSENTIAL_MODEL_CALLS', 'CLAUDE_MODEL'];
2028
- const unsetCmd = profileVarsToReset.map(v => `unset ${v}`).join(' && ');
2029
- execSync(`tmux send-keys -t "${sessionName}" '${unsetCmd}' Enter`, { timeout: 5000 });
2030
+ // Build launch script to avoid tmux send-keys truncation
2031
+ const scriptLines: string[] = ['#!/bin/bash', `cd "${workDir}"`];
2032
+
2033
+ // Unset old profile vars
2034
+ scriptLines.push('unset ANTHROPIC_AUTH_TOKEN ANTHROPIC_BASE_URL ANTHROPIC_SMALL_FAST_MODEL CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC DISABLE_TELEMETRY DISABLE_ERROR_REPORTING DISABLE_AUTOUPDATER DISABLE_NON_ESSENTIAL_MODEL_CALLS CLAUDE_MODEL');
2030
2035
 
2031
- // Set FORGE env vars (short, separate command)
2032
- execSync(`tmux send-keys -t "${sessionName}" 'export FORGE_WORKSPACE_ID="${this.workspaceId}" FORGE_AGENT_ID="${config.id}" FORGE_PORT="${Number(process.env.PORT) || 8403}"' Enter`, { timeout: 5000 });
2036
+ // Set FORGE env vars
2037
+ scriptLines.push(`export FORGE_WORKSPACE_ID="${this.workspaceId}" FORGE_AGENT_ID="${config.id}" FORGE_PORT="${Number(process.env.PORT) || 8403}"`);
2033
2038
 
2034
- // Set profile env vars if any (separate command to avoid truncation)
2039
+ // Set profile env vars
2035
2040
  if (envExports) {
2036
- execSync(`tmux send-keys -t "${sessionName}" '${envExports.replace(/ && $/, '')}' Enter`, { timeout: 5000 });
2041
+ scriptLines.push(envExports.replace(/ && /g, '\n').replace(/\n$/, ''));
2037
2042
  }
2038
2043
 
2039
- // Build CLI start command
2040
- const parts: string[] = [];
2044
+ // Build CLI command
2041
2045
  let cmd = cliCmd;
2042
-
2043
- // Session resume: use bound session ID (primary from project-sessions, others from config)
2044
2046
  if (supportsSession) {
2045
2047
  let sessionId: string | undefined;
2046
-
2047
2048
  if (config.primary) {
2048
2049
  try {
2049
2050
  const { getFixedSession } = await import('../project-sessions') as any;
@@ -2052,7 +2053,6 @@ export class WorkspaceOrchestrator extends EventEmitter {
2052
2053
  } else {
2053
2054
  sessionId = config.boundSessionId;
2054
2055
  }
2055
-
2056
2056
  if (sessionId) {
2057
2057
  const sessionFile = join(this.getCliSessionDir(config.workDir), `${sessionId}.jsonl`);
2058
2058
  if (existsSync(sessionFile)) {
@@ -2061,15 +2061,16 @@ export class WorkspaceOrchestrator extends EventEmitter {
2061
2061
  console.log(`[daemon] ${config.label}: bound session ${sessionId} missing, starting fresh`);
2062
2062
  }
2063
2063
  }
2064
- // No bound session → start fresh (no -c, avoids "No conversation found")
2065
2064
  }
2066
2065
  if (modelFlag) cmd += modelFlag;
2067
2066
  if (config.skipPermissions !== false && skipPermissionsFlag) cmd += ` ${skipPermissionsFlag}`;
2068
2067
  if (mcpConfigFlag) cmd += mcpConfigFlag;
2069
- parts.push(cmd);
2068
+ scriptLines.push(`exec ${cmd}`);
2070
2069
 
2071
- const startCmd = parts.join(' && ');
2072
- execSync(`tmux send-keys -t "${sessionName}" '${startCmd}' Enter`, { timeout: 5000 });
2070
+ // Write script and execute in tmux
2071
+ const scriptPath = `/tmp/forge-launch-${config.id.replace(/[^a-z0-9-]/g, '')}.sh`;
2072
+ writeFileSync(scriptPath, scriptLines.join('\n'), { mode: 0o755 });
2073
+ execSync(`tmux send-keys -t "${sessionName}" 'bash ${scriptPath}' Enter`, { timeout: 5000 });
2073
2074
 
2074
2075
  console.log(`[daemon] ${config.label}: persistent session created (${sessionName}) [${cliType}: ${cliCmd}]`);
2075
2076
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aion0/forge",
3
- "version": "0.5.12",
3
+ "version": "0.5.14",
4
4
  "description": "Unified AI workflow platform — multi-model task orchestration, persistent sessions, web terminal, remote access",
5
5
  "type": "module",
6
6
  "scripts": {