open-agents-ai 0.187.532 → 0.187.534

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
@@ -526818,6 +526818,85 @@ Pick the SMALLEST concrete deliverable from the spec — typically the project e
526818
526818
  }
526819
526819
  }
526820
526820
  }
526821
+ /**
526822
+ * REG-66 Debug-Loop Detection (root-cause from batch535-midi, 2026-05-04).
526823
+ *
526824
+ * Empirical: midi run had 11x `npm run build 2>&1` + same 5 files re-read
526825
+ * 5-6 times each + 22 BFC-61.G coercion BLOCKS — and ZERO of those blocks
526826
+ * resulted in a creative edit. The agent was rationally stuck: it
526827
+ * believed it needed to read more to debug, the build command kept
526828
+ * giving the same error, and the standard "issue an edit" directive
526829
+ * gave no traction because the agent had no concrete edit hypothesis.
526830
+ *
526831
+ * This method analyzes toolCallLog for the debug-loop signature:
526832
+ * - Same shell command stem repeated ≥5 times in the trailing window, OR
526833
+ * - Same file_read path re-read ≥4 times in the trailing window.
526834
+ * Both indicate the agent is reading/running the same things hoping for
526835
+ * different output. Without this signal we'd just keep telling the
526836
+ * agent to "make an edit" — which is exactly what it can't think of.
526837
+ *
526838
+ * When detected, the BFC-61.G block message swaps to a PERTURB-strategy
526839
+ * directive: stop reading, change ONE thing in the most-likely-culprit
526840
+ * file even if you're uncertain, and let the new error signal guide
526841
+ * the next iteration. This is real human debugging strategy ("perturb
526842
+ * to disambiguate"), NOT reward-hacking — the agent still has to
526843
+ * produce a real edit and the success criteria (todos done + build
526844
+ * passing) are unchanged.
526845
+ *
526846
+ * @returns Detection result. `detected=false` → use standard message.
526847
+ * `detected=true` → use REG-66 perturb-strategy message;
526848
+ * `repeatedSample` carries the offending command/path for the
526849
+ * message body so the agent sees the specific pattern called out.
526850
+ */
526851
+ _detectDebugLoop(toolCallLog) {
526852
+ if (process.env["OA_DISABLE_REG66"] === "1")
526853
+ return { detected: false };
526854
+ const WINDOW = 20;
526855
+ const SHELL_REPEAT_THRESHOLD = 5;
526856
+ const READ_REPEAT_THRESHOLD = 4;
526857
+ const window2 = toolCallLog.slice(-WINDOW);
526858
+ if (window2.length < SHELL_REPEAT_THRESHOLD)
526859
+ return { detected: false };
526860
+ const _editClasses = /* @__PURE__ */ new Set(["file_write", "file_edit", "batch_edit", "file_patch"]);
526861
+ for (const c9 of window2) {
526862
+ if (_editClasses.has(c9.name) && c9.success !== false)
526863
+ return { detected: false };
526864
+ }
526865
+ const shellCounts = /* @__PURE__ */ new Map();
526866
+ const readCounts = /* @__PURE__ */ new Map();
526867
+ for (const c9 of window2) {
526868
+ if (c9.name === "shell") {
526869
+ const m2 = c9.argsKey.match(/(?:^|,)command=([^,]+)/);
526870
+ if (m2 && m2[1]) {
526871
+ const stem = m2[1].trim();
526872
+ shellCounts.set(stem, (shellCounts.get(stem) ?? 0) + 1);
526873
+ }
526874
+ } else if (c9.name === "file_read" || c9.name === "file_explore") {
526875
+ const m2 = c9.argsKey.match(/(?:^|,)path=([^,]+)/);
526876
+ if (m2 && m2[1]) {
526877
+ const stem = m2[1].trim();
526878
+ readCounts.set(stem, (readCounts.get(stem) ?? 0) + 1);
526879
+ }
526880
+ }
526881
+ }
526882
+ let bestShell = null;
526883
+ for (const [k, n2] of shellCounts) {
526884
+ if (n2 >= SHELL_REPEAT_THRESHOLD && (!bestShell || n2 > bestShell[1]))
526885
+ bestShell = [k, n2];
526886
+ }
526887
+ let bestRead = null;
526888
+ for (const [k, n2] of readCounts) {
526889
+ if (n2 >= READ_REPEAT_THRESHOLD && (!bestRead || n2 > bestRead[1]))
526890
+ bestRead = [k, n2];
526891
+ }
526892
+ if (bestShell) {
526893
+ return { detected: true, repeatedSample: bestShell[0], count: bestShell[1], kind: "shell" };
526894
+ }
526895
+ if (bestRead) {
526896
+ return { detected: true, repeatedSample: bestRead[0], count: bestRead[1], kind: "read" };
526897
+ }
526898
+ return { detected: false };
526899
+ }
526821
526900
  readSessionTodos() {
526822
526901
  try {
526823
526902
  const sid = process.env["OA_SESSION_ID"] || this._sessionId || "default";
@@ -528818,6 +528897,8 @@ TASK: ${task}` : task;
528818
528897
  try {
528819
528898
  if (this.options.subAgent)
528820
528899
  throw "skip-handoff-subagent";
528900
+ if (process.env["OA_FRESH_SESSION"] === "1")
528901
+ throw "skip-handoff-fresh";
528821
528902
  const oaDir = this._workingDirectory ? _pathJoin(this._workingDirectory, ".oa") : _pathJoin(process.cwd(), ".oa");
528822
528903
  const chainPairs = loadMessagePairsFromLog(oaDir, { currentTask: cleanedTask });
528823
528904
  if (chainPairs.length > 0) {
@@ -530749,7 +530830,32 @@ ${memoryLines.join("\n")}`
530749
530830
  turn,
530750
530831
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
530751
530832
  });
530752
- const reg61BlockMsg = [
530833
+ const _dbgLoop = this._detectDebugLoop(toolCallLog);
530834
+ const _debugLoopSampleSafe = (_dbgLoop.repeatedSample ?? "").slice(0, 120);
530835
+ const reg61BlockMsg = _dbgLoop.detected ? [
530836
+ `[BLOCKED — REG-61 directive in effect — REG-66 DEBUG-LOOP detected]`,
530837
+ ``,
530838
+ `Pattern: ${_dbgLoop.kind === "shell" ? "shell command" : "file"} "${_debugLoopSampleSafe}" was used ${_dbgLoop.count}× in the trailing window with ZERO creative edits landing. You are stuck in a debug loop where re-running / re-reading is producing no new information.`,
530839
+ ``,
530840
+ `STOP DEBUGGING. PERTURB.`,
530841
+ ``,
530842
+ `Strategy when stuck like this (real human debuggers do this):`,
530843
+ ` 1. Pick the source file most likely implicated by the recurring failure (probably in src/, the one most-imported by failing tests).`,
530844
+ ` 2. Pick ONE plausible cause — most-recently-modified line, most-complex function, most-likely-misnamed import, most-likely off-by-one.`,
530845
+ ` 3. Make a SPECULATIVE edit that changes that thing — even if you are NOT certain it'll fix the bug. The point is to get a NEW error signal that disambiguates.`,
530846
+ ` 4. Re-run the failing command. If the error CHANGED, you've learned something. If it's identical, you've ruled out one hypothesis.`,
530847
+ ``,
530848
+ `This is NOT random guessing — it's targeted hypothesis falsification. Reading the same files 5+ times has already proven uninformative; only a state change will move the system.`,
530849
+ ``,
530850
+ `Issue EXACTLY ONE of: file_write / file_edit / batch_edit / file_patch on a single concrete change. The exact CHOICE of edit matters less than NOT continuing to re-read.`,
530851
+ ``,
530852
+ `Allowed bypasses (will not be blocked but will not clear the directive either):`,
530853
+ ` • web_search — search the EXACT recurring error string`,
530854
+ ` • task_complete — exit if you genuinely cannot identify any plausible perturbation`,
530855
+ ` • ask_user — escalate to human (if available)`,
530856
+ ``,
530857
+ `Once you make a real edit, the directive clears and you'll see the new test result.`
530858
+ ].join("\n") : [
530753
530859
  `[BLOCKED — REG-61 directive in effect]`,
530754
530860
  ``,
530755
530861
  `A REG-61 FIRST-EDIT NUDGE was issued earlier and has not yet been satisfied. The directive: your next tool call MUST be a creative edit. You issued '${tc.name}' instead, which is a read/explore/shell call. This call has been BLOCKED.`,
@@ -530777,12 +530883,12 @@ ${memoryLines.join("\n")}`
530777
530883
  });
530778
530884
  this.emit({
530779
530885
  type: "status",
530780
- content: `REG-61 COERCION BLOCK — rejected '${tc.name}' at turn ${turn}; gate stays active until creative edit dispatches`,
530886
+ content: `REG-61 COERCION BLOCK — rejected '${tc.name}' at turn ${turn}; gate stays active until creative edit dispatches${_dbgLoop.detected ? `; REG-66 debug-loop variant (${_dbgLoop.kind} "${_debugLoopSampleSafe.slice(0, 60)}" ${_dbgLoop.count}×)` : ""}`,
530781
530887
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
530782
530888
  });
530783
530889
  this._tagSyntheticFailure({
530784
530890
  mode: "step_repetition",
530785
- rationale: `REG-61 perpetual coercion block on '${tc.name}' — agent ignored FIRST-EDIT NUDGE`
530891
+ rationale: `REG-61 perpetual coercion block on '${tc.name}' — agent ignored FIRST-EDIT NUDGE${_dbgLoop.detected ? " (debug-loop variant)" : ""}`
530786
530892
  });
530787
530893
  return { tc, output: reg61BlockMsg };
530788
530894
  }
@@ -585450,6 +585556,7 @@ async function tryRouteV1(ctx3) {
585450
585556
  const m2 = /^\/v1\/keys\/([^/]+)$/.exec(pathname);
585451
585557
  if (m2 && method === "DELETE") return handleRevokeKey(ctx3, decodeURIComponent(m2[1]));
585452
585558
  }
585559
+ if (pathname === "/v1/share/generate" && method === "POST") return handleGenerateShare(ctx3);
585453
585560
  if (pathname === "/v1/tools" && method === "GET") {
585454
585561
  return handleListTools(ctx3);
585455
585562
  }
@@ -586591,6 +586698,75 @@ async function handleMintKey(ctx3) {
586591
586698
  }
586592
586699
  return true;
586593
586700
  }
586701
+ async function handleGenerateShare(ctx3) {
586702
+ const { req: req2, res, requestId } = ctx3;
586703
+ const reqAuth = req2;
586704
+ if (reqAuth._authScope !== "admin") {
586705
+ sendProblem(res, problemDetails({
586706
+ type: P.forbidden,
586707
+ status: 403,
586708
+ title: "Admin scope required",
586709
+ detail: "Generating a share URL mints a runtime key, which requires 'admin' scope.",
586710
+ instance: requestId
586711
+ }));
586712
+ return true;
586713
+ }
586714
+ try {
586715
+ const body = await parseJsonBodyStrict(req2).catch(() => null) ?? {};
586716
+ const label = typeof body["label"] === "string" && body["label"] ? String(body["label"]).slice(0, 100) : `share-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
586717
+ const scope = typeof body["scope"] === "string" && ["read", "run", "admin"].includes(body["scope"]) ? body["scope"] : "run";
586718
+ const owner = `share:${label}`;
586719
+ const { mintKey: mintKey2 } = await Promise.resolve().then(() => (init_runtime_keys(), runtime_keys_exports));
586720
+ const rec = mintKey2({
586721
+ scope,
586722
+ owner,
586723
+ profile: null
586724
+ });
586725
+ const hostHeader = String(req2.headers["host"] || "").trim();
586726
+ const fallbackHost = process.env["OA_HOST"] || "127.0.0.1:11435";
586727
+ let hostPort = hostHeader || fallbackHost;
586728
+ hostPort = hostPort.replace(/^https?:\/\//i, "");
586729
+ const colonIdx = hostPort.lastIndexOf(":");
586730
+ const host = colonIdx > 0 ? hostPort.slice(0, colonIdx) : hostPort;
586731
+ const portStr = colonIdx > 0 ? hostPort.slice(colonIdx + 1) : "11435";
586732
+ const port = parseInt(portStr, 10) || 11435;
586733
+ const fullKey = rec.key || "";
586734
+ const keyPrefix2 = fullKey.slice(0, 12);
586735
+ if (!fullKey) {
586736
+ sendProblem(res, problemDetails({
586737
+ type: P.internalError,
586738
+ status: 500,
586739
+ title: "Key generation succeeded but no secret returned",
586740
+ detail: "mintKey() did not include the full secret in its response.",
586741
+ instance: requestId
586742
+ }));
586743
+ return true;
586744
+ }
586745
+ const scheme = String(req2.headers["x-forwarded-proto"] || (req2.socket?.encrypted ? "https" : "http"));
586746
+ const shareUrl = `oa-share://${hostPort}#${fullKey}`;
586747
+ const plainUrl = `${scheme}://${hostPort}/?oa-key=${encodeURIComponent(fullKey)}&oa-share-label=${encodeURIComponent(label)}`;
586748
+ sendJson(res, 201, {
586749
+ shareUrl,
586750
+ plainUrl,
586751
+ host,
586752
+ port,
586753
+ key: fullKey,
586754
+ keyPrefix: keyPrefix2,
586755
+ label,
586756
+ issuedAt: rec.created || (/* @__PURE__ */ new Date()).toISOString(),
586757
+ _note: "This is the ONLY response that contains the full key. Hand off the URL now."
586758
+ });
586759
+ } catch (err) {
586760
+ sendProblem(res, problemDetails({
586761
+ type: P.internalError,
586762
+ status: 500,
586763
+ title: "Share URL generation failed",
586764
+ detail: err instanceof Error ? err.message : String(err),
586765
+ instance: requestId
586766
+ }));
586767
+ }
586768
+ return true;
586769
+ }
586594
586770
  async function handleRevokeKey(ctx3, prefix) {
586595
586771
  const { req: req2, res, requestId } = ctx3;
586596
586772
  const reqAuth = req2;
@@ -588907,7 +589083,7 @@ body { display:flex; flex-direction:column; height:100vh; margin:0; overflow:hid
588907
589083
  <span id="sidebar-status-dot" style="width:8px;height:8px;border-radius:50%;background:var(--color-fg-faint);flex-shrink:0" title="Backend connection"></span>
588908
589084
  <span id="sidebar-status-text" style="flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">connecting...</span>
588909
589085
  <button id="sidebar-update-btn" onclick="doUpdate()" style="display:none;background:var(--color-warning);border:none;color:#000;padding:2px 6px;border-radius:var(--radius-sm);font-size:0.6rem;cursor:pointer;font-weight:600">update</button>
588910
- <button id="sidebar-key-btn" onclick="document.getElementById('key-modal').style.display='flex'" title="Set API key" style="background:transparent;border:1px solid var(--color-border);color:var(--color-fg-muted);padding:2px 6px;border-radius:var(--radius-sm);font-size:0.6rem;cursor:pointer">key</button>
589086
+ <button id="sidebar-key-btn" onclick="openKeyModal()" title="Set API key / share access" style="background:transparent;border:1px solid var(--color-border);color:var(--color-fg-muted);padding:2px 6px;border-radius:var(--radius-sm);font-size:0.6rem;cursor:pointer">key</button>
588911
589087
  </div>
588912
589088
 
588913
589089
  <!-- Resize handle -->
@@ -589239,14 +589415,21 @@ body { display:flex; flex-direction:column; height:100vh; margin:0; overflow:hid
589239
589415
  </div>
589240
589416
 
589241
589417
  <div id="key-modal">
589242
- <form class="modal" onsubmit="event.preventDefault(); saveKey();" autocomplete="off">
589243
- <h3>API Key</h3>
589244
- <input id="key-input" type="password" placeholder="Bearer token (leave empty if auth disabled)" autocomplete="new-password">
589245
- <div>
589418
+ <form class="modal" onsubmit="event.preventDefault(); saveKey();" autocomplete="off" style="min-width:480px">
589419
+ <h3>API Key &amp; Sharing</h3>
589420
+ <div style="position:relative">
589421
+ <input id="key-input" type="password" placeholder="Bearer token (leave empty if auth disabled, or paste an oa-share:// URL to connect remote)" autocomplete="new-password" oninput="onKeyInputChange(this.value)" onfocus="showRecentKeysDropdown()" onblur="setTimeout(hideRecentKeysDropdown, 150)">
589422
+ <div id="key-recent-dropdown" style="display:none;position:absolute;top:100%;left:0;right:0;background:var(--color-bg-elevated);border:1px solid var(--color-border);border-radius:var(--radius-sm);max-height:180px;overflow-y:auto;z-index:10;font-size:0.78rem;margin-top:2px"></div>
589423
+ </div>
589424
+ <p id="key-input-hint" style="font-size:0.7rem;color:var(--color-fg-muted);margin:4px 0 8px">Tip: paste an <code>oa-share://host:port#key</code> URL or <code>http://host:port/?oa-key=…</code> to connect to a remote OA instance.</p>
589425
+ <div style="display:flex;gap:6px;flex-wrap:wrap">
589246
589426
  <button type="submit">save</button>
589247
589427
  <button type="button" onclick="clearKey()">clear</button>
589428
+ <button type="button" onclick="generateShareUrl()" title="Generate a share URL that lets a remote OA instance connect to this one">share access</button>
589248
589429
  <button type="button" onclick="closeKeyModal()">cancel</button>
589249
589430
  </div>
589431
+ <div id="share-result" style="display:none;margin-top:14px;padding:10px;background:var(--color-bg-elevated);border:1px solid var(--color-border);border-radius:var(--radius-sm);font-size:0.78rem"></div>
589432
+ <div id="remote-state" style="display:none;margin-top:14px;padding:10px;background:var(--color-bg-elevated);border:1px solid var(--color-accent);border-radius:var(--radius-sm);font-size:0.78rem"></div>
589250
589433
  </form>
589251
589434
  </div>
589252
589435
 
@@ -590368,6 +590551,15 @@ async function sendMessage() {
590368
590551
  messageWithContext = filesBlock + text;
590369
590552
  }
590370
590553
 
590554
+ // FRESH-SESSION: pull the one-shot fresh flag the newChatSession()
590555
+ // handler set. Consumed (cleared) here so subsequent sends within
590556
+ // this same session don't keep claiming "fresh" and miss legitimate
590557
+ // mid-session handoff.
590558
+ let _freshPending = false;
590559
+ try {
590560
+ _freshPending = localStorage.getItem('oa.freshSessionPending') === '1';
590561
+ if (_freshPending) localStorage.removeItem('oa.freshSessionPending');
590562
+ } catch {}
590371
590563
  const body = {
590372
590564
  session_id: chatSessionId,
590373
590565
  model: modelSelect.value,
@@ -590377,6 +590569,10 @@ async function sendMessage() {
590377
590569
  // Pass the user-selected workspace as working_directory so the
590378
590570
  // agent subprocess operates in the right cwd.
590379
590571
  ...(chatWorkingDir ? { working_directory: chatWorkingDir } : {}),
590572
+ // FRESH-SESSION: tell the daemon to skip cross-task handoff
590573
+ // injection on this turn. Daemon plumbs it to OA_FRESH_SESSION=1
590574
+ // for the agent subprocess; runner's handoff block respects it.
590575
+ ...(_freshPending ? { fresh: true } : {}),
590380
590576
  };
590381
590577
 
590382
590578
  const response = await fetch('/v1/chat', {
@@ -590684,8 +590880,33 @@ document.getElementById('key-btn').onclick = () => {
590684
590880
  document.getElementById('key-input').value = apiKey;
590685
590881
  };
590686
590882
  function saveKey() {
590687
- apiKey = document.getElementById('key-input').value;
590883
+ const raw = document.getElementById('key-input').value || '';
590884
+ // SHARE: detect oa-share://host:port#key OR http(s)://host:port/?oa-key=...
590885
+ const parsed = parseShareInput(raw);
590886
+ if (parsed) {
590887
+ // Pasting a share URL → switch into REMOTE mode and reload page
590888
+ // against the remote origin with the key in localStorage.
590889
+ saveRecentKey({ key: parsed.key, host: parsed.host, label: parsed.label || ('remote ' + parsed.host) });
590890
+ try {
590891
+ localStorage.setItem('oa.remoteHost', parsed.host);
590892
+ localStorage.setItem('oa.remoteScheme', parsed.scheme || 'http');
590893
+ localStorage.setItem('oa-api-key', parsed.key);
590894
+ } catch {}
590895
+ // Redirect to the remote host's UI with the key carried in localStorage
590896
+ // (the remote origin uses its own localStorage so we set on first
590897
+ // load there). We open the remote UI in a new tab to preserve the
590898
+ // local session.
590899
+ const remoteUrl = (parsed.scheme || 'http') + '://' + parsed.host + '/?oa-key=' + encodeURIComponent(parsed.key) + '&oa-share-label=' + encodeURIComponent(parsed.label || '');
590900
+ window.open(remoteUrl, '_blank');
590901
+ closeKeyModal();
590902
+ return;
590903
+ }
590904
+ apiKey = raw;
590688
590905
  localStorage.setItem('oa-api-key', apiKey);
590906
+ // Track this key in the recent-keys list so future paste autocompletes it.
590907
+ if (apiKey) {
590908
+ saveRecentKey({ key: apiKey, host: location.host, label: 'local ' + location.host });
590909
+ }
590689
590910
  closeKeyModal();
590690
590911
  loadModels();
590691
590912
  }
@@ -590698,8 +590919,296 @@ function clearKey() {
590698
590919
  }
590699
590920
  function closeKeyModal() {
590700
590921
  document.getElementById('key-modal').classList.remove('visible');
590922
+ const sr = document.getElementById('share-result'); if (sr) sr.style.display = 'none';
590923
+ }
590924
+ function openKeyModal() {
590925
+ document.getElementById('key-modal').classList.add('visible');
590926
+ document.getElementById('key-input').value = apiKey;
590927
+ // Refresh the remote-state hint based on current localStorage.
590928
+ refreshKeyModalRemoteState();
590929
+ }
590930
+ window.openKeyModal = openKeyModal;
590931
+
590932
+ // ─── Share URL parsing ─────────────────────────────────────────────────
590933
+ // Accepts BOTH:
590934
+ // oa-share://[peerId@]host:port#key
590935
+ // http(s)://host:port/?oa-key=KEY[&oa-share-label=LABEL]
590936
+ // Returns { host, key, scheme?, label? } or null when the input is a plain key.
590937
+ function parseShareInput(raw) {
590938
+ const v = String(raw || '').trim();
590939
+ if (!v) return null;
590940
+ // oa-share scheme — use a custom parser since URL() doesn't always honor it.
590941
+ if (v.toLowerCase().startsWith('oa-share://')) {
590942
+ const after = v.slice('oa-share://'.length);
590943
+ const hashIdx = after.indexOf('#');
590944
+ if (hashIdx < 0) return null;
590945
+ const hostPart = after.slice(0, hashIdx);
590946
+ const key = after.slice(hashIdx + 1);
590947
+ if (!hostPart || !key) return null;
590948
+ // Strip optional peerId@ prefix (forward-compat for libp2p tunneling).
590949
+ const atIdx = hostPart.indexOf('@');
590950
+ const host = atIdx >= 0 ? hostPart.slice(atIdx + 1) : hostPart;
590951
+ return { host, key, scheme: 'http' };
590952
+ }
590953
+ // http(s) URL with ?oa-key=...
590954
+ if (/^https?:\\/\\//i.test(v)) {
590955
+ try {
590956
+ const u = new URL(v);
590957
+ const k = u.searchParams.get('oa-key');
590958
+ if (!k) return null;
590959
+ const label = u.searchParams.get('oa-share-label') || '';
590960
+ return { host: u.host, key: k, scheme: u.protocol.replace(':', ''), label };
590961
+ } catch { return null; }
590962
+ }
590963
+ return null;
590964
+ }
590965
+
590966
+ function onKeyInputChange(v) {
590967
+ const hint = document.getElementById('key-input-hint');
590968
+ if (!hint) return;
590969
+ const parsed = parseShareInput(v);
590970
+ if (parsed) {
590971
+ hint.innerHTML = '✓ detected share URL — host <code>' + escapeHtml(parsed.host) + '</code>; clicking save will open the remote UI with this key';
590972
+ hint.style.color = 'var(--color-success)';
590973
+ } else {
590974
+ hint.innerHTML = 'Tip: paste an <code>oa-share://host:port#key</code> URL or <code>http://host:port/?oa-key=…</code> to connect to a remote OA instance.';
590975
+ hint.style.color = 'var(--color-fg-muted)';
590976
+ }
590977
+ showRecentKeysDropdown();
590978
+ }
590979
+
590980
+ // Tiny HTML escape — same as the one used in connections settings.
590981
+ function escapeHtml(s) {
590982
+ return String(s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
590701
590983
  }
590702
590984
 
590985
+ // ─── Recent keys (localStorage history with autocomplete dropdown) ──
590986
+ const _OA_RECENT_KEYS_LS = 'oa.recentKeys';
590987
+ const _OA_RECENT_KEYS_MAX = 10;
590988
+ function loadRecentKeys() {
590989
+ try {
590990
+ const raw = localStorage.getItem(_OA_RECENT_KEYS_LS);
590991
+ if (!raw) return [];
590992
+ const parsed = JSON.parse(raw);
590993
+ return Array.isArray(parsed) ? parsed : [];
590994
+ } catch { return []; }
590995
+ }
590996
+ function saveRecentKey(rec) {
590997
+ if (!rec || !rec.key) return;
590998
+ let list = loadRecentKeys();
590999
+ // Move existing entry with same key to top; otherwise prepend.
591000
+ list = list.filter(r => r.key !== rec.key);
591001
+ list.unshift({ ...rec, lastUsed: new Date().toISOString() });
591002
+ if (list.length > _OA_RECENT_KEYS_MAX) list = list.slice(0, _OA_RECENT_KEYS_MAX);
591003
+ try { localStorage.setItem(_OA_RECENT_KEYS_LS, JSON.stringify(list)); } catch {}
591004
+ }
591005
+ function deleteRecentKey(key) {
591006
+ let list = loadRecentKeys().filter(r => r.key !== key);
591007
+ try { localStorage.setItem(_OA_RECENT_KEYS_LS, JSON.stringify(list)); } catch {}
591008
+ showRecentKeysDropdown();
591009
+ }
591010
+ window.deleteRecentKey = deleteRecentKey;
591011
+
591012
+ function showRecentKeysDropdown() {
591013
+ const dd = document.getElementById('key-recent-dropdown');
591014
+ if (!dd) return;
591015
+ const recents = loadRecentKeys();
591016
+ const inp = document.getElementById('key-input');
591017
+ const filter = (inp && inp.value || '').toLowerCase();
591018
+ const matches = recents.filter(r => {
591019
+ if (!filter) return true;
591020
+ return (r.key || '').toLowerCase().includes(filter)
591021
+ || (r.host || '').toLowerCase().includes(filter)
591022
+ || (r.label || '').toLowerCase().includes(filter);
591023
+ });
591024
+ if (matches.length === 0) { dd.style.display = 'none'; return; }
591025
+ dd.innerHTML = matches.map(r => {
591026
+ const k = String(r.key || '');
591027
+ const masked = k.length > 12 ? k.slice(0, 4) + '…' + k.slice(-4) : k;
591028
+ const host = escapeHtml(r.host || '');
591029
+ const label = escapeHtml(r.label || '');
591030
+ const keySafe = k.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, "\\\\'");
591031
+ return '<div class="oa-rec-key" style="display:flex;align-items:center;gap:8px;padding:6px 10px;cursor:pointer;border-bottom:1px solid var(--color-border)" '
591032
+ + 'onclick="useRecentKey(\\'' + keySafe + '\\')" '
591033
+ + 'onmouseover="this.style.background=\\'var(--color-bg-hover)\\'" '
591034
+ + 'onmouseout="this.style.background=\\'transparent\\'">'
591035
+ + '<span style="flex:1;font-family:var(--font-mono);font-size:0.78rem"><span style="color:var(--color-fg-muted)">' + (host || 'local') + '</span> · <code>' + masked + '</code></span>'
591036
+ + '<span style="font-size:0.7rem;color:var(--color-fg-muted)">' + label + '</span>'
591037
+ + '<button type="button" onclick="event.stopPropagation();deleteRecentKey(\\'' + keySafe + '\\')" '
591038
+ + 'title="Forget this key" '
591039
+ + 'style="background:transparent;border:none;color:var(--color-fg-muted);cursor:pointer;font-size:1rem;padding:0 4px">&times;</button>'
591040
+ + '</div>';
591041
+ }).join('');
591042
+ dd.style.display = 'block';
591043
+ }
591044
+ function hideRecentKeysDropdown() {
591045
+ const dd = document.getElementById('key-recent-dropdown');
591046
+ if (dd) dd.style.display = 'none';
591047
+ }
591048
+ function useRecentKey(k) {
591049
+ const inp = document.getElementById('key-input');
591050
+ if (inp) {
591051
+ inp.value = k;
591052
+ inp.focus();
591053
+ onKeyInputChange(k);
591054
+ }
591055
+ hideRecentKeysDropdown();
591056
+ }
591057
+ window.useRecentKey = useRecentKey;
591058
+
591059
+ // ─── Generate share URL ────────────────────────────────────────────────
591060
+ async function generateShareUrl() {
591061
+ const out = document.getElementById('share-result');
591062
+ if (!out) return;
591063
+ out.style.display = 'block';
591064
+ out.innerHTML = '<div style="color:var(--color-fg-muted)">generating…</div>';
591065
+ try {
591066
+ const r = await fetch('/v1/share/generate', {
591067
+ method: 'POST',
591068
+ headers: { ...headers(), 'Content-Type': 'application/json' },
591069
+ body: JSON.stringify({ scope: 'run' }),
591070
+ });
591071
+ if (r.status === 403) {
591072
+ out.innerHTML = '<div style="color:var(--color-error)">✗ admin scope required — your current key does not have permission to mint share URLs. Set an admin-scope key first.</div>';
591073
+ return;
591074
+ }
591075
+ if (r.status >= 400) {
591076
+ const err = await r.json().catch(() => ({}));
591077
+ out.innerHTML = '<div style="color:var(--color-error)">✗ HTTP ' + r.status + (err.detail ? ' — ' + escapeHtml(err.detail) : '') + '</div>';
591078
+ return;
591079
+ }
591080
+ const j = await r.json();
591081
+ const safeShare = escapeHtml(j.shareUrl || '');
591082
+ const safePlain = escapeHtml(j.plainUrl || '');
591083
+ out.innerHTML =
591084
+ '<div style="font-weight:500;margin-bottom:6px;color:var(--color-success)">✓ Share URL generated — hand off to remote</div>' +
591085
+ '<div style="margin:6px 0">' +
591086
+ '<label style="display:block;font-size:0.7rem;color:var(--color-fg-muted);margin-bottom:2px">oa-share URL (compact, recommended for OA-to-OA)</label>' +
591087
+ '<div style="display:flex;gap:6px;align-items:center">' +
591088
+ '<input readonly value="' + safeShare + '" style="flex:1;background:var(--color-bg-input);border:1px solid var(--color-border);color:var(--color-fg);padding:5px 8px;border-radius:var(--radius-sm);font-family:var(--font-mono);font-size:0.74rem">' +
591089
+ '<button type="button" onclick="copyShareUrl(\\'oa-share\\')" style="font-size:0.74rem">copy</button>' +
591090
+ '</div>' +
591091
+ '</div>' +
591092
+ '<div style="margin:6px 0">' +
591093
+ '<label style="display:block;font-size:0.7rem;color:var(--color-fg-muted);margin-bottom:2px">plain URL (works in any browser, no scheme handler needed)</label>' +
591094
+ '<div style="display:flex;gap:6px;align-items:center">' +
591095
+ '<input readonly value="' + safePlain + '" style="flex:1;background:var(--color-bg-input);border:1px solid var(--color-border);color:var(--color-fg);padding:5px 8px;border-radius:var(--radius-sm);font-family:var(--font-mono);font-size:0.74rem">' +
591096
+ '<button type="button" onclick="copyShareUrl(\\'plain\\')" style="font-size:0.74rem">copy</button>' +
591097
+ '</div>' +
591098
+ '</div>' +
591099
+ '<p style="font-size:0.68rem;color:var(--color-fg-muted);margin:6px 0 0">Note: this URL contains the full secret. Anyone with it can use this OA. To revoke: open Advanced settings → keys → revoke the prefix <code>' + escapeHtml(j.keyPrefix || '') + '</code>.</p>';
591100
+ // Stash for the copy buttons + add to recent.
591101
+ window.__oaLastShareUrl = j.shareUrl;
591102
+ window.__oaLastPlainUrl = j.plainUrl;
591103
+ saveRecentKey({ key: j.key, host: j.host + ':' + j.port, label: j.label || ('shared ' + j.host) });
591104
+ } catch (e) {
591105
+ out.innerHTML = '<div style="color:var(--color-error)">✗ ' + escapeHtml(e && e.message ? e.message : String(e)) + '</div>';
591106
+ }
591107
+ }
591108
+ function copyShareUrl(which) {
591109
+ const v = which === 'plain' ? window.__oaLastPlainUrl : window.__oaLastShareUrl;
591110
+ if (!v) return;
591111
+ if (navigator.clipboard && navigator.clipboard.writeText) {
591112
+ navigator.clipboard.writeText(v).then(
591113
+ () => { /* ok */ },
591114
+ () => { fallbackCopy(v); }
591115
+ );
591116
+ } else {
591117
+ fallbackCopy(v);
591118
+ }
591119
+ }
591120
+ function fallbackCopy(v) {
591121
+ const ta = document.createElement('textarea');
591122
+ ta.value = v; document.body.appendChild(ta); ta.select();
591123
+ try { document.execCommand('copy'); } catch {}
591124
+ document.body.removeChild(ta);
591125
+ }
591126
+ window.generateShareUrl = generateShareUrl;
591127
+ window.copyShareUrl = copyShareUrl;
591128
+
591129
+ // ─── Remote-state helpers (visual indicator on the key button) ───────
591130
+ // When localStorage carries oa.remoteHost we are in REMOTE mode: the
591131
+ // key was loaded from an oa-share URL or via on-load ?oa-key=. The key
591132
+ // button shows a "remote" badge and clicking expands to show host+key
591133
+ // inline with a "close connection" button that severs and shuffles the
591134
+ // key into recents.
591135
+ function refreshKeyModalRemoteState() {
591136
+ const remote = (function() {
591137
+ try { return localStorage.getItem('oa.remoteHost') || ''; } catch { return ''; }
591138
+ })();
591139
+ const stateBox = document.getElementById('remote-state');
591140
+ const btn = document.getElementById('sidebar-key-btn');
591141
+ if (remote && btn) {
591142
+ btn.textContent = 'remote';
591143
+ btn.style.background = 'var(--color-accent)';
591144
+ btn.style.color = '#fff';
591145
+ btn.style.borderColor = 'var(--color-accent)';
591146
+ btn.title = 'connected to remote ' + remote + ' — click to view / disconnect';
591147
+ } else if (btn) {
591148
+ btn.textContent = 'key';
591149
+ btn.style.background = 'transparent';
591150
+ btn.style.color = 'var(--color-fg-muted)';
591151
+ btn.style.borderColor = 'var(--color-border)';
591152
+ btn.title = 'set API key / share access';
591153
+ }
591154
+ if (!stateBox) return;
591155
+ if (remote) {
591156
+ const safeRemote = escapeHtml(remote);
591157
+ stateBox.style.display = 'block';
591158
+ stateBox.innerHTML =
591159
+ '<div style="font-weight:500;color:var(--color-accent)">REMOTE connection active</div>' +
591160
+ '<div style="margin-top:4px;font-size:0.74rem">host <code>' + safeRemote + '</code></div>' +
591161
+ '<div style="margin-top:8px"><button type="button" onclick="closeRemoteConnection()" style="background:var(--color-error);color:#fff;border:none;padding:4px 10px;border-radius:var(--radius-sm);cursor:pointer;font-size:0.74rem">close connection</button></div>';
591162
+ } else {
591163
+ stateBox.style.display = 'none';
591164
+ }
591165
+ }
591166
+ function closeRemoteConnection() {
591167
+ // Move current remote key into recents BEFORE clearing.
591168
+ let savedKey = '';
591169
+ let savedHost = '';
591170
+ try {
591171
+ savedKey = localStorage.getItem('oa-api-key') || '';
591172
+ savedHost = localStorage.getItem('oa.remoteHost') || '';
591173
+ } catch {}
591174
+ if (savedKey) {
591175
+ saveRecentKey({ key: savedKey, host: savedHost, label: 'recent remote ' + savedHost });
591176
+ }
591177
+ try {
591178
+ localStorage.removeItem('oa.remoteHost');
591179
+ localStorage.removeItem('oa.remoteScheme');
591180
+ localStorage.removeItem('oa-api-key');
591181
+ } catch {}
591182
+ apiKey = '';
591183
+ refreshKeyModalRemoteState();
591184
+ // Reload to drop any cached state from the remote target.
591185
+ location.reload();
591186
+ }
591187
+ window.closeRemoteConnection = closeRemoteConnection;
591188
+
591189
+ // ─── On-load: ?oa-key=... pickup ───────────────────────────────────────
591190
+ // When the page loads with an oa-key query param (i.e. someone clicked
591191
+ // a share URL), capture the key into localStorage, mark this as a
591192
+ // remote session (host = this origin), and clean the URL.
591193
+ (function pickupShareKeyFromUrl() {
591194
+ try {
591195
+ const u = new URL(location.href);
591196
+ const k = u.searchParams.get('oa-key');
591197
+ if (!k) return;
591198
+ const label = u.searchParams.get('oa-share-label') || '';
591199
+ localStorage.setItem('oa-api-key', k);
591200
+ // Origin-based remote: when the page loaded from a different host
591201
+ // than the user's "home" OA, that's a remote session by definition.
591202
+ localStorage.setItem('oa.remoteHost', location.host);
591203
+ localStorage.setItem('oa.remoteScheme', location.protocol.replace(':', ''));
591204
+ saveRecentKey({ key: k, host: location.host, label: label || ('remote ' + location.host) });
591205
+ // Strip the key from the URL so it isn't visible / browser-history'd.
591206
+ u.searchParams.delete('oa-key');
591207
+ u.searchParams.delete('oa-share-label');
591208
+ history.replaceState({}, '', u.pathname + (u.search || '') + (u.hash || ''));
591209
+ } catch {}
591210
+ })();
591211
+
590703
591212
  // Tab switching
590704
591213
  const allPanels = ['chat-container','agent-panel','jobs-panel','config-panel','activity-panel','projects-panel','voice-panel'];
590705
591214
  function switchTab(tab) {
@@ -591432,6 +591941,11 @@ function newChatSession() {
591432
591941
  switchSession('');
591433
591942
  const sel = document.getElementById('chat-session-select');
591434
591943
  if (sel) sel.value = '';
591944
+ // FRESH-SESSION: mark the next chat send as fresh so the daemon skips
591945
+ // cross-task handoff injection (which previously bled prior session
591946
+ // context into the new chat regardless of the browser's reset).
591947
+ // Cleared inside the chat send handler after one use.
591948
+ try { localStorage.setItem('oa.freshSessionPending', '1'); } catch {}
591435
591949
  }
591436
591950
  function deleteChatSession() {
591437
591951
  if (!chatSessionId) return;
@@ -592295,6 +592809,7 @@ async function doUpdate() {
592295
592809
  try { pollMetrics(); } catch {}
592296
592810
  try { loadScheduled(); } catch {}
592297
592811
  try { loadServices(); } catch {}
592812
+ try { refreshKeyModalRemoteState(); } catch {}
592298
592813
 
592299
592814
  btn.textContent = 'updated v' + newVersion;
592300
592815
  btn.style.background = '#1a3a1a';
@@ -593279,6 +593794,7 @@ $currentProject.subscribe((proj) => {
593279
593794
  try { if (typeof updateSessionSelect === 'function') updateSessionSelect(); } catch {}
593280
593795
  try { if (typeof updateAgentRunSelect === 'function') updateAgentRunSelect(); } catch {}
593281
593796
  try { if (typeof restoreChatSession === 'function') restoreChatSession(); } catch {}
593797
+ try { if (typeof refreshKeyModalRemoteState === 'function') refreshKeyModalRemoteState(); } catch {}
593282
593798
  });
593283
593799
 
593284
593800
  // $selectedModel changes update the visible <select> if it's not
@@ -599758,6 +600274,15 @@ async function handleRequest(req2, res, ollamaUrl, verbose) {
599758
600274
  });
599759
600275
  return;
599760
600276
  }
600277
+ if (pathname === "/favicon.ico" && method === "GET") {
600278
+ const svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect width="16" height="16" rx="3" fill="#b2920a"/><text x="50%" y="55%" font-size="11" font-family="monospace" font-weight="700" text-anchor="middle" dominant-baseline="middle" fill="#0b0b0b">oa</text></svg>';
600279
+ res.writeHead(200, {
600280
+ "Content-Type": "image/svg+xml",
600281
+ "Cache-Control": "public, max-age=86400"
600282
+ });
600283
+ res.end(svg);
600284
+ return;
600285
+ }
599761
600286
  if (pathname === "/" && method === "GET" && req2.headers.accept?.includes("text/html")) {
599762
600287
  res.writeHead(200, {
599763
600288
  "Content-Type": "text/html; charset=utf-8",
@@ -600881,6 +601406,9 @@ ${historyLines}
600881
601406
  runEnv["OA_RUN_USER"] = req2._authUser || "anonymous";
600882
601407
  runEnv["OA_RUN_SCOPE"] = req2._authScope || "admin";
600883
601408
  runEnv["OA_SESSION_ID"] = session.id;
601409
+ if (chatBody["fresh"] === true) {
601410
+ runEnv["OA_FRESH_SESSION"] = "1";
601411
+ }
600884
601412
  runEnv["OLLAMA_HOST"] = currentCfg.backendUrl || process.env["OLLAMA_HOST"] || "http://127.0.0.1:11434";
600885
601413
  if (currentCfg.apiKey) runEnv["OA_API_KEY_INHERIT"] = currentCfg.apiKey;
600886
601414
  const child = spawn25(process.execPath, [oaBin, ...args], {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.187.532",
3
+ "version": "0.187.534",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "open-agents-ai",
9
- "version": "0.187.532",
9
+ "version": "0.187.534",
10
10
  "hasInstallScript": true,
11
11
  "license": "CC-BY-NC-4.0",
12
12
  "dependencies": {
@@ -2192,9 +2192,9 @@
2192
2192
  }
2193
2193
  },
2194
2194
  "node_modules/bare-url": {
2195
- "version": "2.4.2",
2196
- "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.2.tgz",
2197
- "integrity": "sha512-/9a2j4ac6ckpmAHvod/ob7x439OAHst/drc2Clnq+reRYd/ovddwcF4LfoxHyNk5AuGBnPg+HqFjmE/Zpq6v0A==",
2195
+ "version": "2.4.3",
2196
+ "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.3.tgz",
2197
+ "integrity": "sha512-Kccpc7ACfXaxfeInfqKcZtW4pT5YBn1mesc4sCsun6sRwtbJ4h+sNOaksUpYEJUKfN65YWC6Bw2OJEFiKxq8nQ==",
2198
2198
  "license": "Apache-2.0",
2199
2199
  "optional": true,
2200
2200
  "dependencies": {
@@ -3132,9 +3132,9 @@
3132
3132
  }
3133
3133
  },
3134
3134
  "node_modules/express-rate-limit": {
3135
- "version": "8.4.1",
3136
- "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.4.1.tgz",
3137
- "integrity": "sha512-NGVYwQSAyEQgzxX1iCM978PP9AdO/hW93gMcF6ZwQCm+rFvLsBH6w4xcXWTcliS8La5EPRN3p9wzItqBwJrfNw==",
3135
+ "version": "8.5.0",
3136
+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.0.tgz",
3137
+ "integrity": "sha512-XKhFohWaSBdVJNTi5TaHziqnPkv04I9UQV6q1Wy7Ui6GGQZVW12ojDFwqer14EvCXxjvPG0CyWXx7cAXpALB4Q==",
3138
3138
  "license": "MIT",
3139
3139
  "dependencies": {
3140
3140
  "ip-address": "10.1.0"
@@ -3179,9 +3179,9 @@
3179
3179
  }
3180
3180
  },
3181
3181
  "node_modules/fast-uri": {
3182
- "version": "3.1.1",
3183
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.1.tgz",
3184
- "integrity": "sha512-h2r7rcm6Ee/J8o0LD5djLuFVcfbZxhvho4vvsbeV0aMvXjUgqv4YpxpkEx0d68l6+IleVfLAdVEfhR7QNMkGHQ==",
3182
+ "version": "3.1.2",
3183
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
3184
+ "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
3185
3185
  "funding": [
3186
3186
  {
3187
3187
  "type": "github",
@@ -3638,9 +3638,9 @@
3638
3638
  }
3639
3639
  },
3640
3640
  "node_modules/hono": {
3641
- "version": "4.12.16",
3642
- "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.16.tgz",
3643
- "integrity": "sha512-jN0ZewiNAWSe5khM3EyCmBb250+b40wWbwNILNfEvq84VREWwOIkuUsFONk/3i3nqkz7Oe1PcpM2mwQEK2L9Kg==",
3641
+ "version": "4.12.17",
3642
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.17.tgz",
3643
+ "integrity": "sha512-FbJJNb/XgX7YW0hX/V8w5oYLztKEsRLykCMZWt1WdLtsfjzMvmoqWBA4H4t5norinq8/rh20oiZYr+WSl4UzAQ==",
3644
3644
  "license": "MIT",
3645
3645
  "engines": {
3646
3646
  "node": ">=16.9.0"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-agents-ai",
3
- "version": "0.187.532",
3
+ "version": "0.187.534",
4
4
  "description": "AI coding agent powered by open-source models (Ollama/vLLM) — interactive TUI with agentic tool-calling loop",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",