esque-bridge 0.6.13 → 0.6.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.
Files changed (2) hide show
  1. package/index.js +63 -2
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -191,6 +191,47 @@ function clearCliSessionId(agent, esqueSessionId) {
191
191
  saveSessions();
192
192
  }
193
193
 
194
+ // --- Handoff log ----------------------------------------------------------
195
+ // A running, on-disk record of each turn (prompt + what the agent did), kept in
196
+ // the project folder. Its whole purpose is to survive a lost CLI session: if
197
+ // the agent's conversation memory is ever GC'd, the fresh session is told to
198
+ // read this file and recover the project's context — so a memory reset doesn't
199
+ // mean starting from zero. Best-effort; bounded so it stays readable.
200
+ const HANDOFF_MAX = 48 * 1024;
201
+ function handoffRel(esqueSessionId) {
202
+ const short = String(esqueSessionId || 'default').replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 16) || 'default';
203
+ return `.esque/handoff-${short}.md`;
204
+ }
205
+ function handoffPath(esqueSessionId) {
206
+ return path.join(WORKDIR, handoffRel(esqueSessionId));
207
+ }
208
+ function appendHandoff(esqueSessionId, prompt, replyText) {
209
+ if (!esqueSessionId) return;
210
+ try {
211
+ fs.mkdirSync(path.join(WORKDIR, '.esque'), { recursive: true });
212
+ const file = handoffPath(esqueSessionId);
213
+ const header = fs.existsSync(file)
214
+ ? ''
215
+ : "# Esque handoff log\n\n> A running record of this project's work so your AI agent can recover context if its session is ever reset. Safe to delete (or add `.esque/` to .gitignore).\n\n";
216
+ const reply = String(replyText || '').trim().replace(/\n{3,}/g, '\n\n').slice(0, 1200);
217
+ const stamp = new Date().toISOString();
218
+ fs.appendFileSync(
219
+ file,
220
+ `${header}## ${stamp}\n\n**Prompt:** ${String(prompt || '').slice(0, 600)}\n\n**Agent:** ${reply || '(no output)'}\n\n---\n\n`,
221
+ );
222
+ // Keep it bounded: on overflow, retain the most recent entries (trim to a
223
+ // clean entry boundary so the recovered context reads cleanly).
224
+ const buf = fs.readFileSync(file, 'utf8');
225
+ if (buf.length > HANDOFF_MAX) {
226
+ const tail = buf.slice(buf.length - HANDOFF_MAX);
227
+ const cut = tail.indexOf('\n## ');
228
+ fs.writeFileSync(file, '# Esque handoff log (older entries trimmed)\n\n' + (cut >= 0 ? tail.slice(cut + 1) : tail));
229
+ }
230
+ } catch {
231
+ /* best-effort — never let handoff bookkeeping break a turn */
232
+ }
233
+ }
234
+
194
235
  // --- Adapters -------------------------------------------------------------
195
236
  // Each adapter describes how to invoke a CLI for a single prompt. The
196
237
  // runner is identical across adapters; only argv-building and stdout
@@ -542,7 +583,23 @@ async function runAgentResilient(prompt, esqueSessionId) {
542
583
  if (prevId && RESUME_FAIL_RE.test(String(err && err.message))) {
543
584
  console.warn('[bridge] stored CLI session is gone — retrying as a fresh session');
544
585
  clearCliSessionId(AGENT_TYPE, esqueSessionId);
545
- return runAgent(prompt, esqueSessionId);
586
+ // The agent lost its memory. If we've been keeping a handoff log, point
587
+ // the fresh session at it FIRST so it recovers the project's context
588
+ // instead of starting from zero.
589
+ let recovered = prompt;
590
+ try {
591
+ if (fs.existsSync(handoffPath(esqueSessionId))) {
592
+ recovered =
593
+ `[Esque session recovery] Your previous conversation in this project was reset and its in-memory context was lost. ` +
594
+ `BEFORE anything else, read the file \`${handoffRel(esqueSessionId)}\` in this folder — a running log of everything done in this project so far — and use it to reconstruct context. Then carry out this request:\n\n${prompt}`;
595
+ }
596
+ } catch {
597
+ /* fall back to the bare prompt */
598
+ }
599
+ const fresh = await runAgent(recovered, esqueSessionId);
600
+ // Tell the phone the agent's memory was reset (it ignores the field if
601
+ // unknown). Older bridges never set it.
602
+ return { ...fresh, sessionReset: true };
546
603
  }
547
604
  throw err;
548
605
  }
@@ -1284,11 +1341,13 @@ async function executeHandler(req, res) {
1284
1341
  jobs.set(jobId, {
1285
1342
  status: result.isError ? 'blocked' : 'finished',
1286
1343
  text: result.text,
1344
+ sessionReset: !!result.sessionReset,
1287
1345
  createdAt: Date.now(),
1288
1346
  });
1289
1347
  console.log(
1290
1348
  `[bridge] done job=${jobId} ${result.isError ? 'blocked' : 'finished'}`,
1291
1349
  );
1350
+ if (!result.isError) appendHandoff(esqueSessionId, prompt, result.text);
1292
1351
  const ok = !result.isError;
1293
1352
  sendExpoPush(
1294
1353
  pushToken,
@@ -1335,9 +1394,11 @@ async function executeHandler(req, res) {
1335
1394
 
1336
1395
  try {
1337
1396
  const result = await enqueueRun(prompt, esqueSessionId);
1397
+ if (!result.isError) appendHandoff(esqueSessionId, prompt, result.text);
1338
1398
  finish({
1339
1399
  text: result.text,
1340
1400
  status: result.isError ? 'blocked' : 'finished',
1401
+ sessionReset: !!result.sessionReset,
1341
1402
  });
1342
1403
  } catch (err) {
1343
1404
  console.error('[bridge] error:', err.message);
@@ -1355,7 +1416,7 @@ function resultHandler(req, res) {
1355
1416
  text: 'That task is no longer available — the bridge may have restarted.',
1356
1417
  });
1357
1418
  }
1358
- res.json({ status: job.status, text: job.text });
1419
+ res.json({ status: job.status, text: job.text, sessionReset: !!job.sessionReset });
1359
1420
  }
1360
1421
 
1361
1422
  // Single-flight wrapper for reviving the saved preview: GET /preview and the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "esque-bridge",
3
- "version": "0.6.13",
3
+ "version": "0.6.14",
4
4
  "description": "Desktop-side receiver for the Esque Agent mobile app. Pairs your phone with a local coding-agent CLI (Claude Code, Codex, Aider, or any custom command) via a tunnel + QR code, so prompts run through your subscription instead of per-token API billing.",
5
5
  "bin": {
6
6
  "esque-bridge": "index.js"