@dalmasonto/taskflow-mcp 1.0.25 → 1.0.27

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.
@@ -26,17 +26,26 @@ function detectTmuxPane(pid) {
26
26
  catch { /* tmux not available */ }
27
27
  return null;
28
28
  }
29
- /** Generate a unique agent name from the project folder, auto-suffixing on collision */
29
+ /** Generate a unique agent name from the project folder, auto-suffixing on collision.
30
+ * Cleans up dead entries it encounters so the name is available for INSERT. */
30
31
  function generateName(folderName) {
31
32
  const db = getDb();
32
- const existing = db.prepare('SELECT * FROM agent_registry WHERE name = ?').get(folderName);
33
- if (!existing || !isAlive(existing.pid)) {
33
+ const check = (name) => {
34
+ const row = db.prepare('SELECT * FROM agent_registry WHERE name = ?').get(name);
35
+ if (!row)
36
+ return true; // name is free
37
+ if (!isAlive(row.pid)) {
38
+ // Dead entry — remove it so the name can be reused via INSERT
39
+ db.prepare('DELETE FROM agent_registry WHERE id = ?').run(row.id);
40
+ return true;
41
+ }
42
+ return false; // name is taken by a live agent
43
+ };
44
+ if (check(folderName))
34
45
  return folderName;
35
- }
36
46
  for (let i = 2; i < 100; i++) {
37
47
  const candidate = `${folderName}:${i}`;
38
- const row = db.prepare('SELECT * FROM agent_registry WHERE name = ?').get(candidate);
39
- if (!row || !isAlive(row.pid))
48
+ if (check(candidate))
40
49
  return candidate;
41
50
  }
42
51
  return `${folderName}:${Date.now()}`;
@@ -57,12 +66,36 @@ export function registerAgent(options) {
57
66
  checkAgentLiveness();
58
67
  // Find all entries for this project path
59
68
  const entries = db.prepare('SELECT * FROM agent_registry WHERE project_path = ? ORDER BY connected_at DESC').all(projectPath);
60
- // Priority 1: Reuse a disconnected entry that had the SAME PID (exact session resume)
61
- // Priority 2: Reuse any disconnected entry for this path (new session, same project)
62
- // This prevents a reconnecting agent from stealing another agent's identity
63
- // when two sessions share the same project directory.
64
- const disconnected = entries.find(e => e.status === 'disconnected' && e.pid === agentPid)
65
- || entries.find(e => e.status === 'disconnected');
69
+ // Priority 0: Same PID already connected this agent is renaming itself
70
+ // (e.g. auto-registered as folder name, now calling register_agent with a custom name)
71
+ const samePid = entries.find(e => e.status === 'connected' && e.pid === agentPid);
72
+ if (samePid && options?.customName && options.customName !== samePid.name) {
73
+ const oldName = samePid.name;
74
+ const newName = options.customName;
75
+ // Update agent_messages to preserve history
76
+ db.prepare('UPDATE agent_messages SET sender_name = ? WHERE sender_name = ?').run(newName, oldName);
77
+ db.prepare('UPDATE agent_messages SET recipient_name = ? WHERE recipient_name = ?').run(newName, oldName);
78
+ db.prepare('UPDATE agent_registry SET name = ?, tmux_pane = ?, connected_at = ? WHERE id = ?').run(newName, tmuxPane, ts, samePid.id);
79
+ const row = db.prepare('SELECT * FROM agent_registry WHERE id = ?').get(samePid.id);
80
+ broadcast('agent_renamed', { entity: 'agent', action: 'agent_renamed', payload: { ...row, oldName } });
81
+ logActivity('agent_renamed', `Agent "${oldName}" renamed to "${newName}"`, { entityType: 'agent' });
82
+ return newName;
83
+ }
84
+ // If same PID is already connected with the right name (or no custom name), just return it
85
+ if (samePid) {
86
+ return samePid.name;
87
+ }
88
+ // Reconnection priority for disconnected entries:
89
+ // 1. Custom name matches a disconnected entry exactly — the name IS the stable identity.
90
+ // This is how users resume a specific agent: register_agent({ name: "sentinmail_coder" })
91
+ // 2. Same PID — exact session resume (e.g. MCP server restarted, same shell)
92
+ // 3. Only ONE disconnected entry — unambiguous, safe to reuse
93
+ // When multiple disconnected entries exist and no match is found,
94
+ // we create a fresh entry rather than guessing wrong and mixing up histories.
95
+ const allDisconnected = entries.filter(e => e.status === 'disconnected');
96
+ const disconnected = (options?.customName ? allDisconnected.find(e => e.name === options.customName) : undefined)
97
+ || allDisconnected.find(e => e.pid === agentPid)
98
+ || (allDisconnected.length === 1 ? allDisconnected[0] : undefined);
66
99
  if (disconnected) {
67
100
  const newName = options?.customName || disconnected.name;
68
101
  const renamed = newName !== disconnected.name;
@@ -77,7 +110,7 @@ export function registerAgent(options) {
77
110
  logActivity('agent_connected', `Agent "${newName}" reconnected`, { entityType: 'agent' });
78
111
  return newName;
79
112
  }
80
- // Priority 2: All entries for this path are connected — this is a concurrent agent
113
+ // Priority 3: All entries for this path are connected — this is a concurrent agent
81
114
  // Generate a suffixed name to avoid collision
82
115
  const baseName = options?.customName || folderName;
83
116
  const name = entries.length === 0 ? baseName : generateName(baseName);
@@ -92,14 +92,25 @@ function injectAndMarkDelivered(id, text, tmuxPane) {
92
92
  const db = getDb();
93
93
  db.prepare('UPDATE agent_messages SET delivered = 1 WHERE id = ?').run(id);
94
94
  try {
95
- execFileSync('tmux', ['send-keys', '-t', tmuxPane, text, 'Enter'], { stdio: 'ignore', timeout: 5000 });
96
- // Long text triggers tmux bracketed paste async delay then extra Enter (non-blocking)
95
+ // Send text literally (-l) so special chars are safe and tmux doesn't interpret them
96
+ execFileSync('tmux', ['send-keys', '-t', tmuxPane, '-l', text], { stdio: 'ignore', timeout: 5000 });
97
+ // Delay before Enter — gives the CLI time to fully process the bracketed paste.
98
+ // Without this, Codex (and similar TUIs) may only show partial text because
99
+ // the Enter arrives inside the paste bracket and gets swallowed.
97
100
  setTimeout(() => {
98
101
  try {
99
102
  execFileSync('tmux', ['send-keys', '-t', tmuxPane, 'Enter'], { stdio: 'ignore', timeout: 5000 });
100
103
  }
101
104
  catch { /* pane may have closed */ }
102
- }, 1000);
105
+ // Second Enter after another delay — catches CLIs that need an extra nudge
106
+ // after bracketed paste ends (e.g. long messages that trigger paste mode)
107
+ setTimeout(() => {
108
+ try {
109
+ execFileSync('tmux', ['send-keys', '-t', tmuxPane, 'Enter'], { stdio: 'ignore', timeout: 5000 });
110
+ }
111
+ catch { /* pane may have closed */ }
112
+ }, 500);
113
+ }, 300);
103
114
  console.error(`[bridge] delivered message ${id} to tmux pane ${tmuxPane}`);
104
115
  }
105
116
  catch (err) {
@@ -38,7 +38,7 @@ const DEFAULT_MAX_LOG_ENTRIES = 500;
38
38
  const MILESTONE_ACTIONS = new Set([
39
39
  'task_created', 'task_completed', 'task_partial_done',
40
40
  'project_created', 'project_deleted',
41
- 'agent_connected', 'agent_disconnected',
41
+ 'agent_connected', 'agent_disconnected', 'agent_renamed',
42
42
  ]);
43
43
  export async function compactActivityLog(params) {
44
44
  const db = getDb();
@@ -16,7 +16,7 @@ export async function getAgentInstructions() {
16
16
  'Derive the project name from the **folder name** of the current working directory (e.g. `/home/user/projects/my-app` → search for "my-app"). Use search_projects with that name. If 2–3 results match, **ask the user** which project to use — never guess.',
17
17
  'list_tasks status="in_progress" and status="blocked" for the confirmed project.',
18
18
  'list_notifications unread_only=true',
19
- 'register_agent with a descriptive name for your role (e.g. "backend", "frontend", "lead"). Then list_agents to see who else is online. If other agents are active on the same project, check_messages for any pending messages addressed to you.',
19
+ 'register_agent with a descriptive name for your role (e.g. "backend", "frontend", "lead"). Then list_agents to see who else is online. If 2+ disconnected agents exist for the same project path, ask the user which name to register as — the name preserves message history from previous sessions. If other agents are active on the same project, check_messages for any pending messages addressed to you.',
20
20
  ],
21
21
  state: {
22
22
  projects: projectCount,
@@ -58,8 +58,8 @@ export async function getAgentInstructions() {
58
58
  'The `context` field in ask_user is rendered as Markdown in the UI. Format it well: use `## headings` to organize sections, `**bold**` for key terms, bullet lists for options/trade-offs, and `\\`code\\`` for file paths or commands. Use real newlines (not literal \\\\n). The context should read like a well-written message, not raw debug output.',
59
59
  // ── Multi-Agent Collaboration ──────────────────────────────────────
60
60
  // Identity & discovery
61
- 'On startup, call register_agent with a descriptive name that reflects your role (e.g. "backend", "frontend", "designer", "qa"). If the user assigns you a role, use that. This name is how other agents address you.',
62
- 'Call list_agents to discover who else is online. Before starting work, check if another agent is already working on the same project — coordinate instead of duplicating effort.',
61
+ 'On startup, call register_agent with a descriptive name that reflects your role (e.g. "backend", "frontend", "designer", "qa"). If the user assigns you a role, use that. This name is how other agents address you and how your message history is preserved across sessions.',
62
+ 'Call list_agents to discover who else is online. If you see 2+ disconnected agents on the same project path, ask the user which name to register as — registering with an existing name resumes that agent\'s full message history. Before starting work, check if another agent is already working on the same project — coordinate instead of duplicating effort.',
63
63
  // Communication
64
64
  'Use send_to_agent for fire-and-forget updates — status changes, "I finished task X", "file Y is ready for you". Use ask_agent when you need a response before proceeding — "Should I use REST or GraphQL for this endpoint?", "Is the auth middleware ready?".',
65
65
  'When you receive a message from another agent (via check_messages), respond promptly using respond_to_message. Treat agent messages with the same priority as user messages.',
package/dist/types.d.ts CHANGED
@@ -67,6 +67,7 @@ export declare const ActivityAction: z.ZodEnum<{
67
67
  agent_broadcast: "agent_broadcast";
68
68
  agent_connected: "agent_connected";
69
69
  agent_disconnected: "agent_disconnected";
70
+ agent_renamed: "agent_renamed";
70
71
  terminal_send_keys: "terminal_send_keys";
71
72
  terminal_captured: "terminal_captured";
72
73
  compaction_summary: "compaction_summary";
package/dist/types.js CHANGED
@@ -15,7 +15,7 @@ export const ActivityAction = z.enum([
15
15
  'task_linked', 'task_unlinked', 'dependency_added', 'dependency_removed',
16
16
  'link_added', 'tag_added', 'tag_removed', 'debug_log',
17
17
  'agent_question', 'agent_question_answered', 'agent_broadcast',
18
- 'agent_connected', 'agent_disconnected',
18
+ 'agent_connected', 'agent_disconnected', 'agent_renamed',
19
19
  'terminal_send_keys', 'terminal_captured',
20
20
  'compaction_summary', 'activity_compacted',
21
21
  ]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dalmasonto/taskflow-mcp",
3
- "version": "1.0.25",
3
+ "version": "1.0.27",
4
4
  "description": "MCP server for TaskFlow — manage projects, tasks, timers, analytics via AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",