@shipers-dev/multi 0.6.6 → 0.6.8

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/index.js CHANGED
@@ -5199,6 +5199,7 @@ async function runAcp(opts) {
5199
5199
  const stream2 = ndJsonStream(output, input);
5200
5200
  let activeSessionId = opts.sessionId || null;
5201
5201
  let recording = false;
5202
+ let chunkCount = 0;
5202
5203
  const alwaysAllow = new Set;
5203
5204
  const client = {
5204
5205
  async sessionUpdate(params) {
@@ -5263,12 +5264,33 @@ async function runAcp(opts) {
5263
5264
  }
5264
5265
  }
5265
5266
  recording = true;
5266
- const res = await conn.prompt({
5267
- sessionId: activeSessionId,
5268
- prompt: [{ type: "text", text: opts.prompt }]
5269
- });
5270
- await opts.onEvent({ event_type: "result", payload: { stopReason: res.stopReason } });
5271
- return { stopReason: res.stopReason, sessionId: activeSessionId };
5267
+ const runPrompt = async () => {
5268
+ chunkCount = 0;
5269
+ return await conn.prompt({
5270
+ sessionId: activeSessionId,
5271
+ prompt: [{ type: "text", text: opts.prompt }]
5272
+ });
5273
+ };
5274
+ let res = await runPrompt();
5275
+ let stopReason = res.stopReason;
5276
+ if (chunkCount === 0 && opts.sessionId) {
5277
+ await opts.onEvent({ event_type: "progress", payload: { message: `resumed session produced no output (stopReason=${stopReason}); retrying with fresh session` } });
5278
+ try {
5279
+ const fresh = await conn.newSession({ cwd: opts.cwd || process.cwd(), mcpServers: [] });
5280
+ activeSessionId = fresh.sessionId;
5281
+ if (opts.onSession)
5282
+ await opts.onSession(fresh.sessionId);
5283
+ res = await runPrompt();
5284
+ stopReason = res.stopReason;
5285
+ } catch (e) {
5286
+ await opts.onEvent({ event_type: "error", payload: { message: `retry with fresh session failed: ${String(e)}` } });
5287
+ }
5288
+ }
5289
+ if (chunkCount === 0) {
5290
+ await opts.onEvent({ event_type: "error", payload: { message: `agent produced no output (stopReason=${stopReason})` } });
5291
+ }
5292
+ await opts.onEvent({ event_type: "result", payload: { stopReason } });
5293
+ return { stopReason, sessionId: activeSessionId };
5272
5294
  } finally {
5273
5295
  try {
5274
5296
  child.kill();
@@ -5281,12 +5303,15 @@ async function runAcp(opts) {
5281
5303
  case "agent_message_chunk":
5282
5304
  case "agent_thought_chunk": {
5283
5305
  const text = extractText(u.content);
5284
- if (text)
5306
+ if (text) {
5307
+ chunkCount++;
5285
5308
  await o.onEvent({ event_type: "assistant_text", payload: { text } });
5309
+ }
5286
5310
  break;
5287
5311
  }
5288
5312
  case "tool_call":
5289
5313
  case "tool_call_update": {
5314
+ chunkCount++;
5290
5315
  await o.onEvent({ event_type: "tool_call", payload: {
5291
5316
  id: u.toolCallId || u.id,
5292
5317
  tool: u.title || u.toolName || "tool",
@@ -5574,7 +5599,7 @@ var LOG_PATH2 = join2(MULTI_DIR, "logs", "agent.log");
5574
5599
  var SKILLS_DIR = join2(MULTI_DIR, "skills");
5575
5600
  var STOP_PATH = join2(MULTI_DIR, "stop.flag");
5576
5601
  var TASKS_DB_PATH = join2(MULTI_DIR, "tasks.db");
5577
- var VERSION = "0.6.6";
5602
+ var VERSION = "0.6.7";
5578
5603
  var COMMANDS = {
5579
5604
  setup: "Register this device with a workspace",
5580
5605
  connect: "Connect device to realtime hub and execute assigned tasks",
@@ -6225,11 +6250,18 @@ _${bits.join(" \xB7 ")}_`);
6225
6250
  const base = `${task.title}
6226
6251
 
6227
6252
  ${task.description || ""}`.trim();
6228
- let userPart = task.followup ? `${task.followup}
6253
+ let userPart;
6254
+ if (task.followup) {
6255
+ const cleanFollowup = stripSelfMention(task.followup, preferType);
6256
+ userPart = `# New user message \u2014 THIS is the current request
6257
+
6258
+ ${cleanFollowup}
6229
6259
 
6230
6260
  ---
6231
- Context (original task ${task.key}): ${task.title}` : base || task.title;
6232
- userPart = stripSelfMention(userPart, preferType);
6261
+ *Earlier thread (${task.key} "${task.title}") is only background context. Do not re-address the original task unless the new message explicitly refers back to it.*`;
6262
+ } else {
6263
+ userPart = stripSelfMention(base || task.title, preferType);
6264
+ }
6233
6265
  if (attachmentRefs.length) {
6234
6266
  const lines = attachmentRefs.map((a) => `- ${a.filename}: ${a.path}`).join(`
6235
6267
  `);
@@ -6338,11 +6370,18 @@ ${body}
6338
6370
  const base = `${task.title}
6339
6371
 
6340
6372
  ${task.description || ""}`.trim();
6341
- let userPart = task.followup ? `${task.followup}
6373
+ let userPart;
6374
+ if (task.followup) {
6375
+ const cleanFollowup = stripSelfMention(task.followup, preferType);
6376
+ userPart = `# New user message \u2014 THIS is the current request
6377
+
6378
+ ${cleanFollowup}
6342
6379
 
6343
6380
  ---
6344
- Context (${task.key}): ${task.title}` : base || task.title;
6345
- userPart = stripSelfMention(userPart, preferType);
6381
+ *Earlier thread (${task.key} "${task.title}") is only background context. Do not re-address the original task unless the new message explicitly refers back to it.*`;
6382
+ } else {
6383
+ userPart = stripSelfMention(base || task.title, preferType);
6384
+ }
6346
6385
  if (attachmentRefs.length) {
6347
6386
  const lines = attachmentRefs.map((a) => `- ${a.filename}: ${a.path}`).join(`
6348
6387
  `);
@@ -6381,6 +6420,11 @@ ${userPart}` : userPart;
6381
6420
  if (n > 0)
6382
6421
  log(` \uD83D\uDCCE uploaded ${n} output file(s)`);
6383
6422
  }
6423
+ if (!hasAssistantText && !hadError) {
6424
+ const stopReason = turn.result?.stopReason || "unknown";
6425
+ await postComment(`\u26A0\uFE0F Agent returned no output (stopReason=${stopReason}). Adapter may be stuck on a stale session \u2014 try starting a new issue or clearing session_id.`);
6426
+ log(` \u26A0 ${task.key} produced no assistant output (stopReason=${stopReason})`);
6427
+ }
6384
6428
  if (hadError) {
6385
6429
  await apiClient.post(`${apiUrl}/api/issues/${issueId}/fail`, {});
6386
6430
  log(` \u2717 ${task.key} failed`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipers-dev/multi",
3
- "version": "0.6.6",
3
+ "version": "0.6.8",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "multi-agent": "./dist/index.js"
package/src/acp-runner.ts CHANGED
@@ -57,6 +57,7 @@ export async function runAcp(opts: AcpRunOpts): Promise<{ stopReason: string; se
57
57
 
58
58
  let activeSessionId: string | null = opts.sessionId || null;
59
59
  let recording = false; // only forward events after prompt() starts
60
+ let chunkCount = 0; // assistant_text + tool_call chunks seen during prompt()
60
61
  const alwaysAllow = new Set<string>(); // keys: toolName|kind that user said "always allow"
61
62
 
62
63
  const client: Client = {
@@ -122,13 +123,38 @@ export async function runAcp(opts: AcpRunOpts): Promise<{ stopReason: string; se
122
123
  }
123
124
 
124
125
  recording = true;
125
- const res = await conn.prompt({
126
- sessionId: activeSessionId!,
127
- prompt: [{ type: 'text', text: opts.prompt }],
128
- } as any);
126
+ const runPrompt = async () => {
127
+ chunkCount = 0;
128
+ return await conn.prompt({
129
+ sessionId: activeSessionId!,
130
+ prompt: [{ type: 'text', text: opts.prompt }],
131
+ } as any);
132
+ };
133
+
134
+ let res = await runPrompt();
135
+ let stopReason = (res as any).stopReason;
136
+
137
+ // Resumed session that returned zero chunks → session is likely stale in the adapter.
138
+ // Retry once with a fresh session so user gets an actual response.
139
+ if (chunkCount === 0 && opts.sessionId) {
140
+ await opts.onEvent({ event_type: 'progress', payload: { message: `resumed session produced no output (stopReason=${stopReason}); retrying with fresh session` } });
141
+ try {
142
+ const fresh = await conn.newSession({ cwd: opts.cwd || process.cwd(), mcpServers: [] } as any);
143
+ activeSessionId = fresh.sessionId;
144
+ if (opts.onSession) await opts.onSession(fresh.sessionId);
145
+ res = await runPrompt();
146
+ stopReason = (res as any).stopReason;
147
+ } catch (e) {
148
+ await opts.onEvent({ event_type: 'error', payload: { message: `retry with fresh session failed: ${String(e)}` } });
149
+ }
150
+ }
151
+
152
+ if (chunkCount === 0) {
153
+ await opts.onEvent({ event_type: 'error', payload: { message: `agent produced no output (stopReason=${stopReason})` } });
154
+ }
129
155
 
130
- await opts.onEvent({ event_type: 'result', payload: { stopReason: (res as any).stopReason } });
131
- return { stopReason: (res as any).stopReason, sessionId: activeSessionId! };
156
+ await opts.onEvent({ event_type: 'result', payload: { stopReason } });
157
+ return { stopReason, sessionId: activeSessionId! };
132
158
  } finally {
133
159
  try { child.kill(); } catch {}
134
160
  }
@@ -140,11 +166,12 @@ export async function runAcp(opts: AcpRunOpts): Promise<{ stopReason: string; se
140
166
  case 'agent_message_chunk':
141
167
  case 'agent_thought_chunk': {
142
168
  const text = extractText(u.content);
143
- if (text) await o.onEvent({ event_type: 'assistant_text', payload: { text } });
169
+ if (text) { chunkCount++; await o.onEvent({ event_type: 'assistant_text', payload: { text } }); }
144
170
  break;
145
171
  }
146
172
  case 'tool_call':
147
173
  case 'tool_call_update': {
174
+ chunkCount++;
148
175
  await o.onEvent({ event_type: 'tool_call', payload: {
149
176
  id: u.toolCallId || u.id, tool: u.title || u.toolName || 'tool', kind: u.kind, status: u.status, input: u.rawInput, locations: u.locations,
150
177
  }});
package/src/index.ts CHANGED
@@ -17,7 +17,7 @@ const LOG_PATH = join(MULTI_DIR, 'logs', 'agent.log');
17
17
  const SKILLS_DIR = join(MULTI_DIR, 'skills');
18
18
  const STOP_PATH = join(MULTI_DIR, 'stop.flag');
19
19
  const TASKS_DB_PATH = join(MULTI_DIR, 'tasks.db');
20
- const VERSION = '0.6.6';
20
+ const VERSION = '0.6.7';
21
21
 
22
22
  const COMMANDS = {
23
23
  setup: 'Register this device with a workspace',
@@ -623,10 +623,13 @@ async function handleRunTask(apiUrl: string, deviceId: string, task: any, detect
623
623
  try {
624
624
  if (useAcp) {
625
625
  const base = `${task.title}\n\n${task.description || ''}`.trim();
626
- let userPart = task.followup
627
- ? `${task.followup}\n\n---\nContext (original task ${task.key}): ${task.title}`
628
- : (base || task.title);
629
- userPart = stripSelfMention(userPart, preferType);
626
+ let userPart: string;
627
+ if (task.followup) {
628
+ const cleanFollowup = stripSelfMention(task.followup, preferType);
629
+ userPart = `# New user message — THIS is the current request\n\n${cleanFollowup}\n\n---\n*Earlier thread (${task.key} "${task.title}") is only background context. Do not re-address the original task unless the new message explicitly refers back to it.*`;
630
+ } else {
631
+ userPart = stripSelfMention(base || task.title, preferType);
632
+ }
630
633
  if (attachmentRefs.length) {
631
634
  const lines = attachmentRefs.map(a => `- ${a.filename}: ${a.path}`).join('\n');
632
635
  userPart += `\n\n---\nAttached input files (read them with your tools if useful):\n${lines}\n\nNote: if (and only if) you produce binary or large artifact outputs (screenshots, data exports, generated source files), write them under ${outDir}. Always put your human-facing reply in the chat response itself — do NOT write your answer as a file.`;
@@ -697,8 +700,13 @@ async function handleRunTask(apiUrl: string, deviceId: string, task: any, detect
697
700
  }
698
701
  } catch {}
699
702
  const base = `${task.title}\n\n${task.description || ''}`.trim();
700
- let userPart = task.followup ? `${task.followup}\n\n---\nContext (${task.key}): ${task.title}` : (base || task.title);
701
- userPart = stripSelfMention(userPart, preferType);
703
+ let userPart: string;
704
+ if (task.followup) {
705
+ const cleanFollowup = stripSelfMention(task.followup, preferType);
706
+ userPart = `# New user message — THIS is the current request\n\n${cleanFollowup}\n\n---\n*Earlier thread (${task.key} "${task.title}") is only background context. Do not re-address the original task unless the new message explicitly refers back to it.*`;
707
+ } else {
708
+ userPart = stripSelfMention(base || task.title, preferType);
709
+ }
702
710
  if (attachmentRefs.length) {
703
711
  const lines = attachmentRefs.map(a => `- ${a.filename}: ${a.path}`).join('\n');
704
712
  userPart += `\n\n---\nAttached files:\n${lines}\n\nWrite generated files to: ${outDir}`;
@@ -725,6 +733,13 @@ async function handleRunTask(apiUrl: string, deviceId: string, task: any, detect
725
733
  if (n > 0) log(` 📎 uploaded ${n} output file(s)`);
726
734
  }
727
735
 
736
+ // Visible fallback: agent ran but emitted nothing user-facing
737
+ if (!hasAssistantText && !hadError) {
738
+ const stopReason = turn.result?.stopReason || 'unknown';
739
+ await postComment(`⚠️ Agent returned no output (stopReason=${stopReason}). Adapter may be stuck on a stale session — try starting a new issue or clearing session_id.`);
740
+ log(` ⚠ ${task.key} produced no assistant output (stopReason=${stopReason})`);
741
+ }
742
+
728
743
  if (hadError) { await apiClient.post(`${apiUrl}/api/issues/${issueId}/fail`, {}); log(` ✗ ${task.key} failed`); }
729
744
  else { await apiClient.post(`${apiUrl}/api/issues/${issueId}/complete`, {}); log(` ✓ ${task.key} complete`); }
730
745
  } catch (e) {