@youdie006/prodex 0.40.5 → 0.40.6

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.
@@ -5396,7 +5396,10 @@ export function recentConversationsExpression(limit = 4) {
5396
5396
  }
5397
5397
  const user = chain.find((entry) => entry && entry.author && entry.author.role === "user" && entry.content);
5398
5398
  const text = user ? (user.content.parts || []).filter((part) => typeof part === "string").join("") : "";
5399
- out.push({ id: item.id, userText: text.slice(0, 600) });
5399
+ // Long enough that two consults sharing an opening can still be told
5400
+ // apart by the rest of the prompt; bounded so a huge --file send does
5401
+ // not drag its whole payload back through the bridge.
5402
+ out.push({ id: item.id, userText: text.slice(0, 4000) });
5400
5403
  } catch (error) {
5401
5404
  // A conversation we cannot read is simply not a match.
5402
5405
  }
@@ -5404,9 +5407,47 @@ export function recentConversationsExpression(limit = 4) {
5404
5407
  return out;
5405
5408
  })()`;
5406
5409
  }
5407
- /** Which of those conversations is the one this send posted into, if any. */
5410
+ /**
5411
+ * Which of those conversations is the one this send posted into, if any.
5412
+ *
5413
+ * Identity is a PREFIX test - the first 120 normalized characters - because a
5414
+ * composer tool prefixes the prompt and attachments append to it, so neither
5415
+ * end is reliable on its own. Two consults that open the same way therefore
5416
+ * look identical to it, which is not hypothetical: an agent working from a
5417
+ * template, or a debate loop, repeats its opening every round. Taking the
5418
+ * first match then reads an OLDER conversation and returns its answer as this
5419
+ * send's - measured on two prompts sharing a 125-character preamble and
5420
+ * differing at character 142, where sending the newer one picked the older.
5421
+ *
5422
+ * So an ambiguous prefix is resolved by the whole prompt, and if that cannot
5423
+ * single one out either, nothing is picked. The caller falls back to its other
5424
+ * evidence, which costs a wait; picking wrong costs the wrong answer.
5425
+ */
5408
5426
  export function pickLandedConversation(candidates, sentPrompt) {
5409
- return candidates.find((candidate) => transcriptMatchesSentPrompt(candidate.userText, sentPrompt))?.id;
5427
+ const matches = candidates.filter((candidate) => transcriptMatchesSentPrompt(candidate.userText, sentPrompt));
5428
+ if (matches.length <= 1)
5429
+ return matches[0]?.id;
5430
+ const whole = matches.filter((candidate) => transcriptContainsWholeSentPrompt(candidate.userText, sentPrompt));
5431
+ return whole.length === 1 ? whole[0].id : undefined;
5432
+ }
5433
+ /**
5434
+ * The stricter test, for telling apart conversations the prefix cannot.
5435
+ *
5436
+ * Still a containment test - the transcript wraps the prompt - but of the
5437
+ * whole prompt rather than its opening. A prompt longer than the recorded
5438
+ * sample cannot match, which leaves the caller with nothing to pick, and
5439
+ * nothing is the safe answer here.
5440
+ */
5441
+ export function transcriptContainsWholeSentPrompt(userText, sentPrompt) {
5442
+ const normalize = (value) => value
5443
+ .replace(/\\([\\`*_{}[\]()#+\-.!>~|])/g, "$1")
5444
+ .replace(/\s+/g, " ")
5445
+ .trim();
5446
+ const seen = normalize(userText);
5447
+ const sent = normalize(sentPrompt);
5448
+ if (seen.length === 0 || sent.length === 0)
5449
+ return false;
5450
+ return seen.includes(sent);
5410
5451
  }
5411
5452
  export function transcriptAnswerExpression(conversationId) {
5412
5453
  return `(async () => {
package/dist/cli-pro.js CHANGED
@@ -1352,6 +1352,11 @@ export async function runAskProCommand(rest, io) {
1352
1352
  const autoLoginAllowed = !parsedAskPro.optionArgs.includes("--no-auto-login") &&
1353
1353
  (parsedAskPro.optionArgs.includes("--auto-login") || io.isInteractive === true);
1354
1354
  let consult;
1355
+ // Declared out here so a retry that fails still reports what recovery
1356
+ // did: the notes used to live inside the inner catch and were attached
1357
+ // to the ANSWER, so a recovered browser whose retry then died recorded
1358
+ // nothing about the recovery at all.
1359
+ const recoveryNotes = [];
1355
1360
  try {
1356
1361
  try {
1357
1362
  consult = await sendOnce();
@@ -1360,7 +1365,6 @@ export async function runAskProCommand(rest, io) {
1360
1365
  const firstBlocker = browserSendBlockerFromError(error);
1361
1366
  if (firstBlocker.code !== "browser_unreachable" || !autoLoginAllowed)
1362
1367
  throw error;
1363
- const recoveryNotes = [];
1364
1368
  const recovered = await attemptBrowserAutoRecovery(io.stderr, {
1365
1369
  ...(browserPort !== undefined ? { port: browserPort } : {}),
1366
1370
  notes: recoveryNotes
@@ -1387,7 +1391,7 @@ export async function runAskProCommand(rest, io) {
1387
1391
  // What the send had already noticed before it died. These used to go
1388
1392
  // out with the result, so a failure dropped them - including the note
1389
1393
  // that would explain it, like having just moved off the Work surface.
1390
- const blockedWarnings = sendWarningsFromError(error).map(redactProject);
1394
+ const blockedWarnings = [...recoveryNotes, ...sendWarningsFromError(error)].map(redactProject);
1391
1395
  for (const warning of blockedWarnings)
1392
1396
  io.stderr(warning);
1393
1397
  const persistedBlocker = {
@@ -1797,6 +1801,20 @@ function autoClearDisabledByEnv(env = process.env) {
1797
1801
  const raw = (env.PRODEX_NO_AUTO_CLEAR ?? "").trim().toLowerCase();
1798
1802
  return raw === "1" || raw === "true" || raw === "yes";
1799
1803
  }
1804
+ /**
1805
+ * What recovery did to the browser, in the words the receipt keeps.
1806
+ *
1807
+ * Both halves are recorded because both change what the answer came from. The
1808
+ * ending case takes someone's running browser with it; the launch case is the
1809
+ * quieter one and used to record nothing at all - measured, killing the
1810
+ * dedicated browser and sending with --auto-login recovered in three seconds
1811
+ * and left warnings: [] on the receipt, the result and the task.
1812
+ */
1813
+ export function browserRecoveredNote(ended) {
1814
+ return ended.length > 0
1815
+ ? `browser_recovered: the dedicated browser stopped answering its control port and prodex ended it (pid ${ended.join(", ")}) and started a fresh one before sending. Anything it was doing at the time is gone; the profile and login were kept.`
1816
+ : "browser_recovered: the dedicated browser was not running, so prodex started it with the saved profile before sending. The login was kept; anything the old browser had open is gone.";
1817
+ }
1800
1818
  export async function attemptBrowserAutoRecovery(stderr, options) {
1801
1819
  // Launching is right when the browser is gone and wrong when it is only deaf:
1802
1820
  // a second Chrome on the same profile joins the wedged one rather than
@@ -1831,7 +1849,7 @@ export async function attemptBrowserAutoRecovery(stderr, options) {
1831
1849
  stderr(`recover: the browser stopped answering; ending it (pid ${wedged.join(", ")}) and starting a fresh one...`);
1832
1850
  // Ending someone's browser is not a progress line to scroll past: it goes on
1833
1851
  // the receipt, where an agent or a person reading `pro latest` will see it.
1834
- options.notes?.push(`browser_recovered: the dedicated browser stopped answering its control port and prodex ended it (pid ${wedged.join(", ")}) and started a fresh one before sending. Anything it was doing at the time is gone; the profile and login were kept.`);
1852
+ options.notes?.push(browserRecoveredNote(wedged));
1835
1853
  await endWedgedBrowser(wedged);
1836
1854
  // Wait for the profile lock to actually clear rather than guessing at a
1837
1855
  // delay: the replacement launch fails outright if the old process still
@@ -1874,6 +1892,10 @@ export async function attemptBrowserAutoRecovery(stderr, options) {
1874
1892
  stderr("recover: window minimized again");
1875
1893
  }
1876
1894
  stderr("recover: browser READY - retrying the send...");
1895
+ // The wedged branch above records what it ended; this branch records that
1896
+ // the browser was gone and this answer came from one prodex started.
1897
+ if (wedged.length === 0)
1898
+ options.notes?.push(browserRecoveredNote([]));
1877
1899
  return true;
1878
1900
  }
1879
1901
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youdie006/prodex",
3
- "version": "0.40.5",
3
+ "version": "0.40.6",
4
4
  "description": "Local receipt bus for coordinating Codex execution with ChatGPT Pro/Projects consultation.",
5
5
  "author": "youdie006",
6
6
  "license": "MIT",