@openchambery/web 2.1.8-beta.13 → 2.1.8-beta.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 (48) hide show
  1. package/dist/assets/AssistantView-Dl3YEMqS.js +3 -0
  2. package/dist/assets/{DiagramView-uEy55RKu.js → DiagramView-agoUHATM.js} +1 -1
  3. package/dist/assets/{MarkdownRenderer-im8CTGN6.js → MarkdownRenderer-B-m7eslz.js} +2 -2
  4. package/dist/assets/{MarkdownRendererImpl-C2hDwhv7.js → MarkdownRendererImpl-D1dtzQVw.js} +1 -1
  5. package/dist/assets/{MarkstreamRendererImpl-Dr9BdQWE.js → MarkstreamRendererImpl-DN9LWNCL.js} +1 -1
  6. package/dist/assets/{MobileDetailNavigation-CbtNlmtu.js → MobileDetailNavigation-CajSRGjE.js} +1 -1
  7. package/dist/assets/{MobileShareBridge-DwAzqvuB.js → MobileShareBridge-C7XZWgP7.js} +1 -1
  8. package/dist/assets/{MobileTabPageHeader-CJHZvQz-.js → MobileTabPageHeader-Y0qpxkN_.js} +1 -1
  9. package/dist/assets/{MultiRunWindow-CQZTQVtz.js → MultiRunWindow-I6rfmuYW.js} +1 -1
  10. package/dist/assets/{SettingsView-DrrJ7oYy.js → SettingsView-C86Ay1OO.js} +1 -1
  11. package/dist/assets/{SettingsWindow-BQVvWRUR.js → SettingsWindow-DVAuRy9x.js} +1 -1
  12. package/dist/assets/{TerminalView-DOU_D32e.js → TerminalView-MGeJULsw.js} +1 -1
  13. package/dist/assets/{ToolOutputDialog-CYUmziuT.js → ToolOutputDialog-Cy_pctyN.js} +1 -1
  14. package/dist/assets/{appThemeRegistry-C3Cv_ptr.js → appThemeRegistry-hWeRkCT-.js} +1 -1
  15. package/dist/assets/{fileMentionTouchSelection-C_MaDK_d.js → fileMentionTouchSelection-aHBVlJT7.js} +1 -1
  16. package/dist/assets/{gitApi-DbANgbvG.js → gitApi-DIYbrJQo.js} +1 -1
  17. package/dist/assets/{insertionBoundaries-rMyBgP93.js → insertionBoundaries-BWLb49DE.js} +1 -1
  18. package/dist/assets/{main-BTHlegiK.js → main-DUwpkFVC.js} +2 -2
  19. package/dist/assets/{main-CkuVunGD.js → main-DzbzI4q8.js} +4 -4
  20. package/dist/assets/{mcpQueries-CtFbWoDt.js → mcpQueries-8sLJIXmX.js} +1 -1
  21. package/dist/assets/{miniChat-CAdpZQ_s.js → miniChat-Ohe0kOXn.js} +2 -2
  22. package/dist/assets/{mobile-BXSb2klN.js → mobile-lUObZHLd.js} +2 -2
  23. package/dist/assets/{multirun-CdpCmNeN.js → multirun-Uv7Hvhan.js} +1 -1
  24. package/dist/assets/{projectMeta-DZwOQtmb.js → projectMeta-BYYW3vXx.js} +1 -1
  25. package/dist/assets/{renderElectronMiniChatApp-CdeSYb-O.js → renderElectronMiniChatApp-DxlIY2FE.js} +2 -2
  26. package/dist/assets/renderMobileApp-BJB-2daI.js +28 -0
  27. package/dist/assets/{runtimeConfig-Cik9KmAq.js → runtimeConfig-ZEeKLqoJ.js} +1 -1
  28. package/dist/assets/{runtimeEndpointReset-C82SfSsB.js → runtimeEndpointReset-e9ytf-TV.js} +1 -1
  29. package/dist/assets/{sessionChangeSummary-DxB6UnaZ.js → sessionChangeSummary-BR23tPPl.js} +1 -1
  30. package/dist/assets/{sessionLookup-DI87dntm.js → sessionLookup-jd6sOrlf.js} +1 -1
  31. package/dist/assets/{speechSettingsQueries-DNVZNmwi.js → speechSettingsQueries-CGhwyKcU.js} +1 -1
  32. package/dist/assets/{useAppFontEffects-DEPF5G_u.js → useAppFontEffects-BHvYYFZ6.js} +5 -5
  33. package/dist/assets/{useDesktopWindowControlsLayout-CuS4YKQc.js → useDesktopWindowControlsLayout-B2L-_6kZ.js} +1 -1
  34. package/dist/assets/{useEffectiveDirectory-DqK9wN2A.js → useEffectiveDirectory-DV_VjkHO.js} +1 -1
  35. package/dist/assets/{useMobileNavigationStore-BO8WLtu9.js → useMobileNavigationStore-BwyVZbQQ.js} +3 -3
  36. package/dist/assets/{useWorkerHighlightedLines-C7PH1cfT.js → useWorkerHighlightedLines-BycjZGWp.js} +1 -1
  37. package/dist/index.html +2 -2
  38. package/dist/mini-chat.html +2 -2
  39. package/dist/mobile.html +2 -2
  40. package/package.json +1 -1
  41. package/server/lib/assistants/DOCUMENTATION.md +13 -2
  42. package/server/lib/assistants/contact-attachments.http.test.js +2 -2
  43. package/server/lib/assistants/contact-store.js +11 -1
  44. package/server/lib/assistants/contact-store.test.js +27 -8
  45. package/server/lib/assistants/service.js +6 -67
  46. package/server/lib/assistants/service.test.js +20 -5
  47. package/dist/assets/AssistantView-Clf1fwkk.js +0 -3
  48. package/dist/assets/renderMobileApp-GFT3rzlb.js +0 -28
@@ -1,4 +1,4 @@
1
- import{r as e}from"./rolldown-runtime-hePW80VL.js";import{n as t}from"./runtime-fetch-QCWYlAx0.js";import{Mn as n,ln as r}from"./persistence-B4Ni3Gmv.js";import{it as i,rt as a}from"./vendor-base-ui-CdzJ6My_.js";import{n as o}from"./platform-BQPsr8Zl.js";import{v as s}from"./useUIStore-CDMI_Yzu.js";import{n as c}from"./vendor-react-BP2cRVqX.js";import{a as l,n as u}from"./vendor-aparajita-capacitor-secure-storage-xol_6BsB.js";import{f as d,r as f,t as p}from"./button-BupmKVUs.js";import{n as m}from"./useThemeSystem-8jhE4Zyk.js";import{n as h}from"./Icon-B4xKSuz3.js";import{i as g,n as _,t as v}from"./tooltip-Dbgnq-Mh.js";import{a as y,i as b,o as x,t as S}from"./image-loading-CsxDGgek.js";import{n as C}from"./MarkdownRenderer-im8CTGN6.js";import{n as w,o as T,s as E,t as D}from"./markdown-worker-DFNZ3KJ_.js";import{n as ee}from"./markdownTheme-CV7_0QAL.js";import{t as te}from"./vendor-tanstack-react-virtual-B_EO--9t.js";var O=e(i(),1),k=c(),A=a(),ne=`w-[min(30rem,calc(100cqw-1rem))] min-w-[280px] max-w-full overflow-hidden rounded-xl shadow-[inset_0_1px_0_0_rgba(255,255,255,0.8),inset_0_0_0_1px_rgba(0,0,0,0.04),0_0_0_1px_rgba(0,0,0,0.10),0_1px_2px_-0.5px_rgba(0,0,0,0.08),0_4px_8px_-2px_rgba(0,0,0,0.08),0_12px_20px_-4px_rgba(0,0,0,0.08)] dark:shadow-[inset_0_1px_0_0_rgba(255,255,255,0.12),inset_0_0_0_1px_rgba(255,255,255,0.08),0_0_0_1px_rgba(0,0,0,0.36),0_1px_1px_-0.5px_rgba(0,0,0,0.22),0_3px_3px_-1.5px_rgba(0,0,0,0.20),0_6px_6px_-3px_rgba(0,0,0,0.16)]`,j={maxWidth:`min(28rem, calc(100cqw - 4ch))`,backgroundColor:`var(--surface-elevated)`,color:`var(--surface-elevated-foreground)`},M=`flex items-center justify-between gap-3 border-b border-[var(--surface-subtle)] px-3 py-2.5`,N=`text-xs font-medium leading-4 text-foreground md:text-[0.8125rem] md:leading-5`,re=`text-xs tabular-nums leading-4 text-muted-foreground md:text-[0.8125rem] md:leading-5`,P=`m-0 list-none divide-y divide-[var(--surface-subtle)] p-0`,F=`${P} max-h-[min(22rem,50vh)] overflow-y-auto`,ie=`${P} w-full`,ae={className:`p-0 rounded-none`,maxHeightClass:`max-h-[46vh]`,disableHorizontal:!0},I=`px-2.5 py-1.5 text-xs leading-4 md:py-2 md:text-[0.8125rem] md:leading-5`,oe=`flex min-w-0 items-center gap-1 text-xs leading-4 text-muted-foreground md:gap-1.5 md:text-[0.8125rem] md:leading-5`,se=`min-w-0 truncate text-xs leading-4 text-foreground md:text-[0.8125rem] md:leading-5`,ce=`inline-flex shrink-0 items-center gap-1 text-xs leading-4 tabular-nums text-muted-foreground md:text-[0.8125rem] md:leading-5`,le=`h-3 w-3 shrink-0 md:h-3.5 md:w-3.5`,ue={in_progress:`text-foreground`,pending:`text-foreground`,completed:`text-muted-foreground line-through`,cancelled:`text-muted-foreground line-through`},L={in_progress:`chat.statusRow.todo.status.inProgress`,pending:`chat.statusRow.todo.status.pending`,completed:`chat.statusRow.todo.status.completed`,cancelled:`chat.statusRow.todo.status.cancelled`},R=e=>{let t=(0,k.c)(26),{todo:n,isActive:r,activeRef:i}=e,{t:a}=d(),o=z.has(n.status)?n.status:`pending`,s=L[o],c;t[0]!==a||t[1]!==s?(c=a(s),t[0]=a,t[1]=s,t[2]=c):c=t[2];let l=c,u;t[3]===o?u=t[4]:(u=o===`in_progress`?(0,A.jsx)(h,{name:`record-circle`,className:`h-3.5 w-3.5 text-[var(--status-info)]`,"aria-hidden":`true`}):o===`completed`?(0,A.jsx)(h,{name:`checkbox-circle`,className:`h-3.5 w-3.5 text-[var(--status-success)]`,"aria-hidden":`true`}):(0,A.jsx)(h,{name:`time`,className:`h-3.5 w-3.5 text-muted-foreground`,"aria-hidden":`true`}),t[3]=o,t[4]=u);let p=u,m=r?`step`:void 0,y=r&&`bg-[var(--surface-muted)] before:absolute before:inset-y-2 before:left-0 before:w-0.5 before:rounded-full before:bg-[var(--status-info)]`,b;t[5]===y?b=t[6]:(b=f(`group relative grid min-w-0 grid-cols-[1rem_minmax(0,1fr)] items-start gap-2.5`,I,y),t[5]=y,t[6]=b);let x;t[7]!==p||t[8]!==l?(x=(0,A.jsx)(g,{asChild:!0,children:(0,A.jsx)(`span`,{className:`flex size-4 items-center justify-center`,"aria-label":l,children:p})}),t[7]=p,t[8]=l,t[9]=x):x=t[9];let S;t[10]===l?S=t[11]:(S=(0,A.jsx)(_,{side:`left`,sideOffset:6,children:l}),t[10]=l,t[11]=S);let C;t[12]!==x||t[13]!==S?(C=(0,A.jsxs)(v,{children:[x,S]}),t[12]=x,t[13]=S,t[14]=C):C=t[14];let w=ue[o],T;t[15]===w?T=t[16]:(T=f(`block whitespace-normal break-words`,w),t[15]=w,t[16]=T);let E;t[17]!==T||t[18]!==n.content?(E=(0,A.jsx)(`span`,{className:`min-w-0 flex-1`,children:(0,A.jsx)(`span`,{className:T,children:n.content})}),t[17]=T,t[18]=n.content,t[19]=E):E=t[19];let D;return t[20]!==i||t[21]!==E||t[22]!==m||t[23]!==b||t[24]!==C?(D=(0,A.jsxs)(`li`,{ref:i,"aria-current":m,className:b,children:[C,E]}),t[20]=i,t[21]=E,t[22]=m,t[23]=b,t[24]=C,t[25]=D):D=t[25],D},z=new Set([`in_progress`,`pending`,`completed`,`cancelled`]),B=`image/png`,V=l(`OpenChamberMedia`),H=e=>{let t=e.toLowerCase().split(`;`)[0]?.trim()||B;if(t===`image/jpeg`||t===`image/jpg`)return`jpg`;if(t===`image/webp`)return`webp`;if(t===`image/gif`)return`gif`;if(t===`image/svg+xml`)return`svg`;if(t===`image/png`)return`png`;let n=t.split(`/`)[1];return n&&/^[a-z0-9.+-]+$/i.test(n)?n:`png`},U=(e,t)=>{let n=`image.${H(t)}`,r=(((e||``).trim()||n).split(/[\\/]/).pop()||n).replace(/[^\w.\- ()[\]]+/g,`_`).replace(/^\.+/,``);return r?/\.[a-z0-9]{1,8}$/i.test(r)?r:`${r}.${H(t)}`:n},W=e=>new Promise((t,n)=>{let r=new FileReader;r.onload=()=>{if(typeof r.result==`string`){t(r.result);return}n(Error(`Failed to encode image`))},r.onerror=()=>n(r.error??Error(`Failed to encode image`)),r.readAsDataURL(e)}),de=async e=>{let t=await W(e),n=t.indexOf(`,`);return n>=0?t.slice(n+1):t},fe=(e,t)=>{let n=URL.createObjectURL(e),r=document.createElement(`a`);r.href=n,r.download=t,document.body.appendChild(r),r.click(),document.body.removeChild(r),window.setTimeout(()=>URL.revokeObjectURL(n),1e3)},G=async(e,t)=>{let n=await fetch(e,{signal:t,cache:`no-store`});if(!n.ok)throw Error(`Image fetch failed (${n.status})`);return n.blob()},pe=async(e,t=B)=>{if(!e.naturalWidth||!e.naturalHeight)return null;let n=e.currentSrc||e.src;if(n&&(/^(?:blob:|data:|openchamber-asset:)/i.test(n)||/^https?:/i.test(n)||/^\/api(?=\/|[?#]|$)/.test(n)))try{let e=await G(n);if(e.size>0)return e}catch{}try{let n=document.createElement(`canvas`);n.width=e.naturalWidth,n.height=e.naturalHeight;let r=n.getContext(`2d`);if(!r)return null;r.drawImage(e,0,0);let i=t.startsWith(`image/`)?t:B,a=await new Promise(e=>{n.toBlob(t=>e(t),i===`image/jpg`?`image/jpeg`:i)});return a&&a.size>0?a:null}catch{return null}},me=async(e,t)=>{let n=``;try{return n=await S(e,t??new AbortController().signal),await G(n,t)}finally{n&&y(n)}},he=async(e,n)=>{let r=e.effectiveDirectory??``,i=null;e.prefetchedBlob&&e.prefetchedBlob.size>0&&(i=e.prefetchedBlob);let a=e.displayUrl?.trim();if(!i&&a)try{i=await G(a,n)}catch{i=null}if(!i){let a=e.sourceUrl.trim();if(!a)throw Error(`Image source is empty`);if(/^(?:data|blob):/i.test(a)||/^https?:/i.test(a)||/^\/api(?=\/|[?#]|$)/.test(a))i=await G(a,n);else{let e=x(a,r);if(b(e))try{i=await me(e.path,n)}catch{let r=await t(`/api/fs/raw`,{method:`GET`,cache:`no-store`,query:{path:e.path},signal:n});if(!r.ok)throw Error(`Image source request failed with status ${r.status}`);i=await r.blob()}else i=await G(e.source,n)}}if(!i||i.size<=0)throw Error(`Image is empty`);let o=(e.mimeType||(i.type&&i.type.startsWith(`image/`)?i.type:``)||B).split(`;`)[0].trim()||B;return{blob:i,filename:U(e.filename,o),mimeType:o}},ge=()=>!o()||u===void 0?!1:u.isPluginAvailable(`OpenChamberMedia`),_e=async(e,t)=>{let{blob:i,filename:a,mimeType:o}=await he(e,t);if(ge()){let e=await de(i);return await V.saveImage({dataBase64:e,mimeType:o,filename:a}),`saved`}if(r()){let e=n()?.vscode;if(e?.saveImage){let t=await W(i);return await e.saveImage({fileName:a,dataUrl:t}),`saved`}}return fe(i,a),`downloaded`},ve=e=>e.replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`).replace(/"/g,`&quot;`).replace(/'/g,`&#39;`),ye=e=>e?Object.entries(e).map(([e,t])=>t==null?``:`${e.startsWith(`--`)?e:e.replace(/[A-Z]/g,e=>`-${e.toLowerCase()}`)}:${typeof t==`number`?`${t}px`:t};`).join(``):``,K=(e,t,n)=>{let r=e.querySelector(`pre`);r&&(r.style.margin=`0`,r.style.background=`transparent`,r.style.whiteSpace=t?`pre-wrap`:`pre`,t&&(r.style.wordBreak=`break-word`,r.style.overflowWrap=`break-word`));let i=e.querySelector(`code`);if(i){let e=ye(n);e&&i.setAttribute(`style`,`${i.getAttribute(`style`)??``}${e}`)}},be=e=>`<pre><code>${ve(e)}</code></pre>`,xe=e=>{let t=(0,k.c)(21),{code:n,language:r,className:i,style:a,codeStyle:o,wrap:s}=e,c=s!==void 0&&s,{currentTheme:l}=m(),u=O.useRef(null),d;t[0]===l?d=t[1]:(d=ee(l),t[0]=l,t[1]=d);let p=d,h,g;t[2]!==n||t[3]!==o||t[4]!==c?(h=()=>{let e=u.current;e&&(e.innerHTML=be(n),K(e,c,o))},g=[n,c,o],t[2]=n,t[3]=o,t[4]=c,t[5]=h,t[6]=g):(h=t[5],g=t[6]),O.useLayoutEffect(h,g);let _,v;t[7]!==n||t[8]!==o||t[9]!==r||t[10]!==c?(_=()=>{let e=u.current;if(!e)return;let t=!0,i=new AbortController;return D(n,(r||`text`).toLowerCase(),{signal:i.signal,priority:`visible`}).then(n=>{!t||!e||!n||(e.innerHTML=n,K(e,c,o))}),()=>{t=!1,i.abort()}},v=[n,r,c,o],t[7]=n,t[8]=o,t[9]=r,t[10]=c,t[11]=_,t[12]=v):(_=t[11],v=t[12]),O.useEffect(_,v);let y;t[13]===i?y=t[14]:(y=f(`typography-code`,i),t[13]=i,t[14]=y);let b=p,x;t[15]!==a||t[16]!==b?(x={...b,...a},t[15]=a,t[16]=b,t[17]=x):x=t[17];let S;return t[18]!==y||t[19]!==x?(S=(0,A.jsx)(`div`,{ref:u,className:y,style:x}),t[18]=y,t[19]=x,t[20]=S):S=t[20],S},Se=e=>{let t=e.replace(/^<file>\s*\n?/,``).replace(/\n?<\/file>\s*$/,``);return t=t.replace(/^\s*\d{5}\|\s?/gm,``),t.trim()},Ce=(e,t=``)=>{if(typeof e==`string`)return e;if(e==null)return t;if(typeof e==`number`||typeof e==`boolean`||typeof e==`bigint`)return String(e);try{return JSON.stringify(e)}catch{return t}},we=e=>e?e.includes(`<diagnostics`)||e.includes(`<file_diagnostics>`)||e.includes(`LSP errors detected`)||e.includes(`This file has errors`):!1,Te=e=>e?e.replace(/\n{0,2}LSP errors detected[\s\S]*?<diagnostics[^>]*>[\s\S]*?<\/diagnostics>/g,``).replace(/\n{0,2}This file has errors[\s\S]*?<\/file_diagnostics>/g,``).replace(/<diagnostics[^>]*>[\s\S]*?<\/diagnostics>/g,``).replace(/<file_diagnostics>[\s\S]*?<\/file_diagnostics>/g,``).trim():``,Ee=(e,t)=>!e||typeof e!=`object`?String(e):E(e,t||``),q=e=>{if(typeof e==`string`){let t=e.trim();return t.length>0?t:void 0}if(e&&typeof e==`object`){let t=e.patch;if(typeof t==`string`){let e=t.trim();return e.length>0?e:void 0}}},De=e=>{if(!e||typeof e!=`object`)return;let t=q(e.patch)??q(e.diff);if(t)return t;let n=Array.isArray(e.files)?e.files:[];for(let e of n){if(!e||typeof e!=`object`)continue;let t=q(e.patch)??q(e.diff);if(t)return t}},Oe=e=>{if(!e||typeof e!=`string`)return{data:null,isJson:!1};let t=e.trim();if(!t.startsWith(`{`)&&!t.startsWith(`[`)||!t.endsWith(`}`)&&!t.endsWith(`]`)||t.length<2)return{data:null,isJson:!1};try{let e=JSON.parse(t);return typeof e==`object`&&e?{data:e,isJson:!0}:{data:null,isJson:!1}}catch{return{data:null,isJson:!1}}},ke=(e,t,n)=>{let r=Se(e);if((t===`edit`||t===`multiedit`||t===`write`||t===`apply_patch`)&&we(r)&&(r=Te(r)),(t===`edit`||t===`multiedit`||t===`apply_patch`)&&r.trim().length===0){let e=De(n);if(e)return e}return r},Ae=e=>{let t=e.match(/<type>(file|directory)<\/type>/i)?.[1]?.toLowerCase()??`unknown`,n=(e.match(/<content>([\s\S]*?)<\/content>/i)?.[1]??e).replace(/\r\n/g,`
1
+ import{r as e}from"./rolldown-runtime-hePW80VL.js";import{n as t}from"./runtime-fetch-QCWYlAx0.js";import{Mn as n,ln as r}from"./persistence-B4Ni3Gmv.js";import{it as i,rt as a}from"./vendor-base-ui-CdzJ6My_.js";import{n as o}from"./platform-BQPsr8Zl.js";import{v as s}from"./useUIStore-CDMI_Yzu.js";import{n as c}from"./vendor-react-BP2cRVqX.js";import{a as l,n as u}from"./vendor-aparajita-capacitor-secure-storage-xol_6BsB.js";import{f as d,r as f,t as p}from"./button-BupmKVUs.js";import{n as m}from"./useThemeSystem-8jhE4Zyk.js";import{n as h}from"./Icon-B4xKSuz3.js";import{i as g,n as _,t as v}from"./tooltip-Dbgnq-Mh.js";import{a as y,i as b,o as x,t as S}from"./image-loading-CsxDGgek.js";import{n as C}from"./MarkdownRenderer-B-m7eslz.js";import{n as w,o as T,s as E,t as D}from"./markdown-worker-DFNZ3KJ_.js";import{n as ee}from"./markdownTheme-CV7_0QAL.js";import{t as te}from"./vendor-tanstack-react-virtual-B_EO--9t.js";var O=e(i(),1),k=c(),A=a(),ne=`w-[min(30rem,calc(100cqw-1rem))] min-w-[280px] max-w-full overflow-hidden rounded-xl shadow-[inset_0_1px_0_0_rgba(255,255,255,0.8),inset_0_0_0_1px_rgba(0,0,0,0.04),0_0_0_1px_rgba(0,0,0,0.10),0_1px_2px_-0.5px_rgba(0,0,0,0.08),0_4px_8px_-2px_rgba(0,0,0,0.08),0_12px_20px_-4px_rgba(0,0,0,0.08)] dark:shadow-[inset_0_1px_0_0_rgba(255,255,255,0.12),inset_0_0_0_1px_rgba(255,255,255,0.08),0_0_0_1px_rgba(0,0,0,0.36),0_1px_1px_-0.5px_rgba(0,0,0,0.22),0_3px_3px_-1.5px_rgba(0,0,0,0.20),0_6px_6px_-3px_rgba(0,0,0,0.16)]`,j={maxWidth:`min(28rem, calc(100cqw - 4ch))`,backgroundColor:`var(--surface-elevated)`,color:`var(--surface-elevated-foreground)`},M=`flex items-center justify-between gap-3 border-b border-[var(--surface-subtle)] px-3 py-2.5`,N=`text-xs font-medium leading-4 text-foreground md:text-[0.8125rem] md:leading-5`,re=`text-xs tabular-nums leading-4 text-muted-foreground md:text-[0.8125rem] md:leading-5`,P=`m-0 list-none divide-y divide-[var(--surface-subtle)] p-0`,F=`${P} max-h-[min(22rem,50vh)] overflow-y-auto`,ie=`${P} w-full`,ae={className:`p-0 rounded-none`,maxHeightClass:`max-h-[46vh]`,disableHorizontal:!0},I=`px-2.5 py-1.5 text-xs leading-4 md:py-2 md:text-[0.8125rem] md:leading-5`,oe=`flex min-w-0 items-center gap-1 text-xs leading-4 text-muted-foreground md:gap-1.5 md:text-[0.8125rem] md:leading-5`,se=`min-w-0 truncate text-xs leading-4 text-foreground md:text-[0.8125rem] md:leading-5`,ce=`inline-flex shrink-0 items-center gap-1 text-xs leading-4 tabular-nums text-muted-foreground md:text-[0.8125rem] md:leading-5`,le=`h-3 w-3 shrink-0 md:h-3.5 md:w-3.5`,ue={in_progress:`text-foreground`,pending:`text-foreground`,completed:`text-muted-foreground line-through`,cancelled:`text-muted-foreground line-through`},L={in_progress:`chat.statusRow.todo.status.inProgress`,pending:`chat.statusRow.todo.status.pending`,completed:`chat.statusRow.todo.status.completed`,cancelled:`chat.statusRow.todo.status.cancelled`},R=e=>{let t=(0,k.c)(26),{todo:n,isActive:r,activeRef:i}=e,{t:a}=d(),o=z.has(n.status)?n.status:`pending`,s=L[o],c;t[0]!==a||t[1]!==s?(c=a(s),t[0]=a,t[1]=s,t[2]=c):c=t[2];let l=c,u;t[3]===o?u=t[4]:(u=o===`in_progress`?(0,A.jsx)(h,{name:`record-circle`,className:`h-3.5 w-3.5 text-[var(--status-info)]`,"aria-hidden":`true`}):o===`completed`?(0,A.jsx)(h,{name:`checkbox-circle`,className:`h-3.5 w-3.5 text-[var(--status-success)]`,"aria-hidden":`true`}):(0,A.jsx)(h,{name:`time`,className:`h-3.5 w-3.5 text-muted-foreground`,"aria-hidden":`true`}),t[3]=o,t[4]=u);let p=u,m=r?`step`:void 0,y=r&&`bg-[var(--surface-muted)] before:absolute before:inset-y-2 before:left-0 before:w-0.5 before:rounded-full before:bg-[var(--status-info)]`,b;t[5]===y?b=t[6]:(b=f(`group relative grid min-w-0 grid-cols-[1rem_minmax(0,1fr)] items-start gap-2.5`,I,y),t[5]=y,t[6]=b);let x;t[7]!==p||t[8]!==l?(x=(0,A.jsx)(g,{asChild:!0,children:(0,A.jsx)(`span`,{className:`flex size-4 items-center justify-center`,"aria-label":l,children:p})}),t[7]=p,t[8]=l,t[9]=x):x=t[9];let S;t[10]===l?S=t[11]:(S=(0,A.jsx)(_,{side:`left`,sideOffset:6,children:l}),t[10]=l,t[11]=S);let C;t[12]!==x||t[13]!==S?(C=(0,A.jsxs)(v,{children:[x,S]}),t[12]=x,t[13]=S,t[14]=C):C=t[14];let w=ue[o],T;t[15]===w?T=t[16]:(T=f(`block whitespace-normal break-words`,w),t[15]=w,t[16]=T);let E;t[17]!==T||t[18]!==n.content?(E=(0,A.jsx)(`span`,{className:`min-w-0 flex-1`,children:(0,A.jsx)(`span`,{className:T,children:n.content})}),t[17]=T,t[18]=n.content,t[19]=E):E=t[19];let D;return t[20]!==i||t[21]!==E||t[22]!==m||t[23]!==b||t[24]!==C?(D=(0,A.jsxs)(`li`,{ref:i,"aria-current":m,className:b,children:[C,E]}),t[20]=i,t[21]=E,t[22]=m,t[23]=b,t[24]=C,t[25]=D):D=t[25],D},z=new Set([`in_progress`,`pending`,`completed`,`cancelled`]),B=`image/png`,V=l(`OpenChamberMedia`),H=e=>{let t=e.toLowerCase().split(`;`)[0]?.trim()||B;if(t===`image/jpeg`||t===`image/jpg`)return`jpg`;if(t===`image/webp`)return`webp`;if(t===`image/gif`)return`gif`;if(t===`image/svg+xml`)return`svg`;if(t===`image/png`)return`png`;let n=t.split(`/`)[1];return n&&/^[a-z0-9.+-]+$/i.test(n)?n:`png`},U=(e,t)=>{let n=`image.${H(t)}`,r=(((e||``).trim()||n).split(/[\\/]/).pop()||n).replace(/[^\w.\- ()[\]]+/g,`_`).replace(/^\.+/,``);return r?/\.[a-z0-9]{1,8}$/i.test(r)?r:`${r}.${H(t)}`:n},W=e=>new Promise((t,n)=>{let r=new FileReader;r.onload=()=>{if(typeof r.result==`string`){t(r.result);return}n(Error(`Failed to encode image`))},r.onerror=()=>n(r.error??Error(`Failed to encode image`)),r.readAsDataURL(e)}),de=async e=>{let t=await W(e),n=t.indexOf(`,`);return n>=0?t.slice(n+1):t},fe=(e,t)=>{let n=URL.createObjectURL(e),r=document.createElement(`a`);r.href=n,r.download=t,document.body.appendChild(r),r.click(),document.body.removeChild(r),window.setTimeout(()=>URL.revokeObjectURL(n),1e3)},G=async(e,t)=>{let n=await fetch(e,{signal:t,cache:`no-store`});if(!n.ok)throw Error(`Image fetch failed (${n.status})`);return n.blob()},pe=async(e,t=B)=>{if(!e.naturalWidth||!e.naturalHeight)return null;let n=e.currentSrc||e.src;if(n&&(/^(?:blob:|data:|openchamber-asset:)/i.test(n)||/^https?:/i.test(n)||/^\/api(?=\/|[?#]|$)/.test(n)))try{let e=await G(n);if(e.size>0)return e}catch{}try{let n=document.createElement(`canvas`);n.width=e.naturalWidth,n.height=e.naturalHeight;let r=n.getContext(`2d`);if(!r)return null;r.drawImage(e,0,0);let i=t.startsWith(`image/`)?t:B,a=await new Promise(e=>{n.toBlob(t=>e(t),i===`image/jpg`?`image/jpeg`:i)});return a&&a.size>0?a:null}catch{return null}},me=async(e,t)=>{let n=``;try{return n=await S(e,t??new AbortController().signal),await G(n,t)}finally{n&&y(n)}},he=async(e,n)=>{let r=e.effectiveDirectory??``,i=null;e.prefetchedBlob&&e.prefetchedBlob.size>0&&(i=e.prefetchedBlob);let a=e.displayUrl?.trim();if(!i&&a)try{i=await G(a,n)}catch{i=null}if(!i){let a=e.sourceUrl.trim();if(!a)throw Error(`Image source is empty`);if(/^(?:data|blob):/i.test(a)||/^https?:/i.test(a)||/^\/api(?=\/|[?#]|$)/.test(a))i=await G(a,n);else{let e=x(a,r);if(b(e))try{i=await me(e.path,n)}catch{let r=await t(`/api/fs/raw`,{method:`GET`,cache:`no-store`,query:{path:e.path},signal:n});if(!r.ok)throw Error(`Image source request failed with status ${r.status}`);i=await r.blob()}else i=await G(e.source,n)}}if(!i||i.size<=0)throw Error(`Image is empty`);let o=(e.mimeType||(i.type&&i.type.startsWith(`image/`)?i.type:``)||B).split(`;`)[0].trim()||B;return{blob:i,filename:U(e.filename,o),mimeType:o}},ge=()=>!o()||u===void 0?!1:u.isPluginAvailable(`OpenChamberMedia`),_e=async(e,t)=>{let{blob:i,filename:a,mimeType:o}=await he(e,t);if(ge()){let e=await de(i);return await V.saveImage({dataBase64:e,mimeType:o,filename:a}),`saved`}if(r()){let e=n()?.vscode;if(e?.saveImage){let t=await W(i);return await e.saveImage({fileName:a,dataUrl:t}),`saved`}}return fe(i,a),`downloaded`},ve=e=>e.replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`).replace(/"/g,`&quot;`).replace(/'/g,`&#39;`),ye=e=>e?Object.entries(e).map(([e,t])=>t==null?``:`${e.startsWith(`--`)?e:e.replace(/[A-Z]/g,e=>`-${e.toLowerCase()}`)}:${typeof t==`number`?`${t}px`:t};`).join(``):``,K=(e,t,n)=>{let r=e.querySelector(`pre`);r&&(r.style.margin=`0`,r.style.background=`transparent`,r.style.whiteSpace=t?`pre-wrap`:`pre`,t&&(r.style.wordBreak=`break-word`,r.style.overflowWrap=`break-word`));let i=e.querySelector(`code`);if(i){let e=ye(n);e&&i.setAttribute(`style`,`${i.getAttribute(`style`)??``}${e}`)}},be=e=>`<pre><code>${ve(e)}</code></pre>`,xe=e=>{let t=(0,k.c)(21),{code:n,language:r,className:i,style:a,codeStyle:o,wrap:s}=e,c=s!==void 0&&s,{currentTheme:l}=m(),u=O.useRef(null),d;t[0]===l?d=t[1]:(d=ee(l),t[0]=l,t[1]=d);let p=d,h,g;t[2]!==n||t[3]!==o||t[4]!==c?(h=()=>{let e=u.current;e&&(e.innerHTML=be(n),K(e,c,o))},g=[n,c,o],t[2]=n,t[3]=o,t[4]=c,t[5]=h,t[6]=g):(h=t[5],g=t[6]),O.useLayoutEffect(h,g);let _,v;t[7]!==n||t[8]!==o||t[9]!==r||t[10]!==c?(_=()=>{let e=u.current;if(!e)return;let t=!0,i=new AbortController;return D(n,(r||`text`).toLowerCase(),{signal:i.signal,priority:`visible`}).then(n=>{!t||!e||!n||(e.innerHTML=n,K(e,c,o))}),()=>{t=!1,i.abort()}},v=[n,r,c,o],t[7]=n,t[8]=o,t[9]=r,t[10]=c,t[11]=_,t[12]=v):(_=t[11],v=t[12]),O.useEffect(_,v);let y;t[13]===i?y=t[14]:(y=f(`typography-code`,i),t[13]=i,t[14]=y);let b=p,x;t[15]!==a||t[16]!==b?(x={...b,...a},t[15]=a,t[16]=b,t[17]=x):x=t[17];let S;return t[18]!==y||t[19]!==x?(S=(0,A.jsx)(`div`,{ref:u,className:y,style:x}),t[18]=y,t[19]=x,t[20]=S):S=t[20],S},Se=e=>{let t=e.replace(/^<file>\s*\n?/,``).replace(/\n?<\/file>\s*$/,``);return t=t.replace(/^\s*\d{5}\|\s?/gm,``),t.trim()},Ce=(e,t=``)=>{if(typeof e==`string`)return e;if(e==null)return t;if(typeof e==`number`||typeof e==`boolean`||typeof e==`bigint`)return String(e);try{return JSON.stringify(e)}catch{return t}},we=e=>e?e.includes(`<diagnostics`)||e.includes(`<file_diagnostics>`)||e.includes(`LSP errors detected`)||e.includes(`This file has errors`):!1,Te=e=>e?e.replace(/\n{0,2}LSP errors detected[\s\S]*?<diagnostics[^>]*>[\s\S]*?<\/diagnostics>/g,``).replace(/\n{0,2}This file has errors[\s\S]*?<\/file_diagnostics>/g,``).replace(/<diagnostics[^>]*>[\s\S]*?<\/diagnostics>/g,``).replace(/<file_diagnostics>[\s\S]*?<\/file_diagnostics>/g,``).trim():``,Ee=(e,t)=>!e||typeof e!=`object`?String(e):E(e,t||``),q=e=>{if(typeof e==`string`){let t=e.trim();return t.length>0?t:void 0}if(e&&typeof e==`object`){let t=e.patch;if(typeof t==`string`){let e=t.trim();return e.length>0?e:void 0}}},De=e=>{if(!e||typeof e!=`object`)return;let t=q(e.patch)??q(e.diff);if(t)return t;let n=Array.isArray(e.files)?e.files:[];for(let e of n){if(!e||typeof e!=`object`)continue;let t=q(e.patch)??q(e.diff);if(t)return t}},Oe=e=>{if(!e||typeof e!=`string`)return{data:null,isJson:!1};let t=e.trim();if(!t.startsWith(`{`)&&!t.startsWith(`[`)||!t.endsWith(`}`)&&!t.endsWith(`]`)||t.length<2)return{data:null,isJson:!1};try{let e=JSON.parse(t);return typeof e==`object`&&e?{data:e,isJson:!0}:{data:null,isJson:!1}}catch{return{data:null,isJson:!1}}},ke=(e,t,n)=>{let r=Se(e);if((t===`edit`||t===`multiedit`||t===`write`||t===`apply_patch`)&&we(r)&&(r=Te(r)),(t===`edit`||t===`multiedit`||t===`apply_patch`)&&r.trim().length===0){let e=De(n);if(e)return e}return r},Ae=e=>{let t=e.match(/<type>(file|directory)<\/type>/i)?.[1]?.toLowerCase()??`unknown`,n=(e.match(/<content>([\s\S]*?)<\/content>/i)?.[1]??e).replace(/\r\n/g,`
2
2
  `).split(`
3
3
  `),r=e=>/\(\s*File has more lines\..*offset.*\)/i.test(e.trim());return{type:t,lines:n.map(e=>{let n=e.trim(),i=n.startsWith(`(`)&&n.endsWith(`)`)||r(n);if(t!==`directory`){let t=e.match(/^(\d+):\s?(.*)$/);if(t){let e=t[2],n=e.trim(),i=n.startsWith(`(`)&&n.endsWith(`)`)||r(n);return{lineNumber:i?null:Number(t[1]),text:e,isInfo:i}}}return{lineNumber:null,text:e,isInfo:i}}).filter((e,t,n)=>{if(e.text.trim().length>0)return!0;let r=n[t-1],i=n[t+1],a=!!(r?.isInfo||i?.isInfo),o=e.lineNumber!==null;return!(a&&o)})}},je=(e,t)=>{try{let n=e.trim().split(`
4
4
  `).filter(Boolean);if(n.length===0)return null;let r=[];return n.forEach(e=>{let t=e.match(/^(\s*)(.+)$/);if(t){let[,e,n]=t,i=Math.floor(e.length/2),a=!n.endsWith(`/`);r.push({name:n.replace(/\/$/,``),depth:i,isFile:a})}}),(0,A.jsx)(`div`,{className:f(`w-full min-w-0 font-mono space-y-0.5`,t?.unstyled?null:`p-3 bg-muted/20 rounded-xl border border-border/30`),style:s.tool.popup,children:r.map((e,t)=>(0,A.jsx)(`div`,{className:`min-w-0`,style:{paddingLeft:`${e.depth*20}px`},children:e.isFile?(0,A.jsx)(`span`,{className:`text-foreground/90 block truncate`,children:e.name}):(0,A.jsxs)(`span`,{className:`font-semibold text-foreground block truncate`,children:[e.name,`/`]})},t))})}catch{return null}},Me={backgroundColor:`var(--status-info)`,opacity:.6},Ne=(e,t,n)=>{try{let r=e.trim().split(`
package/dist/index.html CHANGED
@@ -552,7 +552,7 @@
552
552
  opacity: 0.72;
553
553
  }
554
554
  </style>
555
- <script type="module" crossorigin src="/assets/main-BTHlegiK.js"></script>
555
+ <script type="module" crossorigin src="/assets/main-DUwpkFVC.js"></script>
556
556
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
557
557
  <link rel="modulepreload" crossorigin href="/assets/runtime-auth-C8w0nlKy.js">
558
558
  <link rel="modulepreload" crossorigin href="/assets/runtime-switch-3ad3yzM7.js">
@@ -572,7 +572,7 @@
572
572
  <link rel="modulepreload" crossorigin href="/assets/terminalApi-kRMttlcP.js">
573
573
  <link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
574
574
  <link rel="modulepreload" crossorigin href="/assets/capgoAdapter-KNLHDj7G.js">
575
- <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-Cik9KmAq.js">
575
+ <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-ZEeKLqoJ.js">
576
576
  <link rel="stylesheet" crossorigin href="/assets/src-CdGit3gf.css">
577
577
  </head>
578
578
  <body class="h-full bg-background text-foreground">
@@ -74,7 +74,7 @@
74
74
  }
75
75
  </style>
76
76
 
77
- <script type="module" crossorigin src="/assets/miniChat-CAdpZQ_s.js"></script>
77
+ <script type="module" crossorigin src="/assets/miniChat-Ohe0kOXn.js"></script>
78
78
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
79
79
  <link rel="modulepreload" crossorigin href="/assets/runtime-auth-C8w0nlKy.js">
80
80
  <link rel="modulepreload" crossorigin href="/assets/runtime-switch-3ad3yzM7.js">
@@ -94,7 +94,7 @@
94
94
  <link rel="modulepreload" crossorigin href="/assets/terminalApi-kRMttlcP.js">
95
95
  <link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
96
96
  <link rel="modulepreload" crossorigin href="/assets/capgoAdapter-KNLHDj7G.js">
97
- <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-Cik9KmAq.js">
97
+ <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-ZEeKLqoJ.js">
98
98
  <link rel="stylesheet" crossorigin href="/assets/src-CdGit3gf.css">
99
99
  </head>
100
100
  <body class="h-full bg-background text-foreground">
package/dist/mobile.html CHANGED
@@ -63,7 +63,7 @@
63
63
  })();
64
64
  </script>
65
65
 
66
- <script type="module" crossorigin src="/assets/mobile-BXSb2klN.js"></script>
66
+ <script type="module" crossorigin src="/assets/mobile-lUObZHLd.js"></script>
67
67
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
68
68
  <link rel="modulepreload" crossorigin href="/assets/runtime-auth-C8w0nlKy.js">
69
69
  <link rel="modulepreload" crossorigin href="/assets/runtime-switch-3ad3yzM7.js">
@@ -83,7 +83,7 @@
83
83
  <link rel="modulepreload" crossorigin href="/assets/terminalApi-kRMttlcP.js">
84
84
  <link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
85
85
  <link rel="modulepreload" crossorigin href="/assets/capgoAdapter-KNLHDj7G.js">
86
- <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-Cik9KmAq.js">
86
+ <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-ZEeKLqoJ.js">
87
87
  <link rel="stylesheet" crossorigin href="/assets/src-CdGit3gf.css">
88
88
  </head>
89
89
  <body class="h-full bg-background text-foreground">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openchambery/web",
3
- "version": "2.1.8-beta.13",
3
+ "version": "2.1.8-beta.14",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./server/index.js",
@@ -1,5 +1,16 @@
1
1
  # Assistants Module
2
2
 
3
+ ## Transient contact errors
4
+
5
+ Contact-turn failures are delivered through `openchamber:contact-turn-end` only,
6
+ never inserted into the durable contact transcript. The store rejects `status:error`
7
+ writes and atomically deletes legacy error messages and their parts during startup;
8
+ a cleanup failure rolls back and fails startup instead of exposing partial history.
9
+ User messages and already-published valid replies/cards remain intact. Live clients
10
+ show temporary error feedback until a newer message arrives; reconnect/restart does
11
+ not recover errors. Errors do not become list previews, unread counts, or LLM memory.
12
+ Worker-card execution status remains part of the card's authoritative state.
13
+
3
14
  ## Avatar identity
4
15
 
5
16
  `avatarSeed` is an optional nonempty string (maximum 256 characters) on create
@@ -141,7 +152,7 @@ and keep their existing error result handling.
141
152
  - The model is prompted to announce its next action and report verified milestones during execution, then give a short final outcome. Public messages share the existing SSE, durable contact rows, and turn-global indices across web, desktop, VS Code, hosted mobile, and Capacitor. No timer narration, tool logs, progress widgets, or synthetic chat messages are added. Timing of model-authored milestones remains model policy; the transport guarantees immediate delivery once an explicit public record arrives.
142
153
  - Completion notifications use the latest spoken message, keeping earlier progress out of the final notification body. Transcript persistence reconciles by stable bubble IDs and adds only remaining messages.
143
154
  - Tool results remain model context for every tool. The outcome projector uses assistant-authored text only. Terminal operations retain the mutation stop gate, followed by a tools-disabled completion on the same provider/model to communicate the actual result. A failed/empty summary surfaces a turn error; a completed side effect stays committed and is never replayed by that summary pass.
144
- - Reset summaries receive only the successful reset tool result, excluding prior messages and attachments. The service retains one model-generated confirmation and discards pre-reset output for that turn. Existing structured failure rows retain diagnostics for recovery; the UI renders those as separate error feedback, outside assistant speech bubbles.
155
+ - Reset summaries receive only the successful reset tool result, excluding prior messages and attachments. The service retains one model-generated confirmation and discards pre-reset output for that turn. Failures emit temporary live error feedback outside assistant speech bubbles, without a persisted failure row.
145
156
 
146
157
  - Protocol correction never replays rejected assistant prose into model history: simulated User/Assistant turns and tool-result records are rejected as fabricated evidence. Known rejected coding calls cannot be skipped with a subsequent `complete` declaration; the model must issue actual calls or report a blocker. Corrections remain bounded and exhaustion is an error, not a completion.
147
158
 
@@ -178,7 +189,7 @@ and keep their existing error result handling.
178
189
  - Worker sessions stamp `metadata.openchamber.assigned` (`from: 'contact'`, `assistantID`, `name`). They must **not** use `openchamber.assistant.assistantID` — that marker hides archived Assistant bindings from Chat.
179
190
  - `AssistantDTO.assignedSessionIDs` lists in-flight (`busy`/`question`) watches for assigned worker sessions. Session-card busy stays on the card; it does **not** drive the list green dot.
180
191
  - **Contact active turn (server authoritative):** process-local metadata tracks admission → `queued` → `running` → settled (removed). `AssistantDTO.working` is true only while that assistant has an unsettled contact turn. `AssistantDTO.activeContactTurn` is `{ turnID, messageID, status: 'queued'|'running', admittedAt }` or `null` so APP restart / reconnect can rehydrate list green dots and the contact 3-dot row from the snapshot HTTP GET. Spoken bubbles and cards persist to SQLite as soon as they publish (`done:true` / `onCard`) so another device or APP restart can GET them while the turn is still running. Token-level SSE deltas remain live paint only. A server process restart clears in-memory activity (no permanent green; tools are not auto-rerun) but already-published bubbles/cards stay in the transcript. Per-assistant lanes isolate turns across assistants/runtimes. Stale `contact-turn-end` for an older `turnID` must not clear a newer active turn.
181
- - **List latest-message preview (server authoritative):** every `AssistantDTO` path (`snapshot` / `create` / `update` / other `output` rows) carries `latestMessagePreview: { messageID, ordinal, createdAt, role: 'user'|'assistant', text, fallbackKind: 'image'|'file'|'session'|'assistant'|'schedule'|null } | null`. Source is the contact transcript only (not `defaultPrompt` / `updatedAt`). Order is the same keyset as contact pages: newest `(ordinal DESC, message_id DESC)`. Visible roles are `user`, `assistant`, and `peer` (peer maps to `role: 'user'` because inbound DMs are user-visible). Internal assigned-session settle markers (`oc.settle.*`) are excluded in the SQL candidate probe (and again in `buildContactMessagePreview`). Durable error bubbles keep their real body text. Plain text is sanitized and capped at `CONTACT_PREVIEW_MAX_CHARS` (500) with no data-URL/base64 bodies. File-only rows set `fallbackKind` `image`/`file`; pure cards set `session`/`assistant`/`schedule`. Empty transcript → `null`. **Snapshot cost:** one prepared statement reused per assistant (`assistant_id` + indexed `ORDER BY ordinal DESC, message_id DESC LIMIT candidateLimit`, catalog ≤100) collecting only those bounded candidates, then **one** parts `IN` query — not a window/`ROW_NUMBER` over full multi-assistant history and not HTTP N+1. SQL already drops pure settle tails so they cannot exhaust the LIMIT and hide an older real bubble; remaining non-preview empty rows may still consume a candidate slot within that same LIMIT. Admission, assistant completion, peer DM, card insert, wipe, and clear-memory already bump domain revision / `openchamber:assistants-changed`; clients re-GET snapshot for the new preview. Clear-memory keeps transcript rows so the preview still reflects the last bubble; clear-history / reset remove rows so the preview becomes the wipe confirm (or `null` after a full unbounded wipe with no remaining user).
192
+ - **List latest-message preview (server authoritative):** every `AssistantDTO` path (`snapshot` / `create` / `update` / other `output` rows) carries `latestMessagePreview: { messageID, ordinal, createdAt, role: 'user'|'assistant', text, fallbackKind: 'image'|'file'|'session'|'assistant'|'schedule'|null } | null`. Source is the contact transcript only (not `defaultPrompt` / `updatedAt`). Order is the same keyset as contact pages: newest `(ordinal DESC, message_id DESC)`. Visible roles are `user`, `assistant`, and `peer` (peer maps to `role: 'user'` because inbound DMs are user-visible). Internal assigned-session settle markers (`oc.settle.*`) are excluded in the SQL candidate probe (and again in `buildContactMessagePreview`). Transient errors are never stored and cannot replace the preview. Plain text is sanitized and capped at `CONTACT_PREVIEW_MAX_CHARS` (500) with no data-URL/base64 bodies. File-only rows set `fallbackKind` `image`/`file`; pure cards set `session`/`assistant`/`schedule`. Empty transcript → `null`. **Snapshot cost:** one prepared statement reused per assistant (`assistant_id` + indexed `ORDER BY ordinal DESC, message_id DESC LIMIT candidateLimit`, catalog ≤100) collecting only those bounded candidates, then **one** parts `IN` query — not a window/`ROW_NUMBER` over full multi-assistant history and not HTTP N+1. SQL already drops pure settle tails so they cannot exhaust the LIMIT and hide an older real bubble; remaining non-preview empty rows may still consume a candidate slot within that same LIMIT. Admission, assistant completion, peer DM, card insert, wipe, and clear-memory already bump domain revision / `openchamber:assistants-changed`; clients re-GET snapshot for the new preview. Clear-memory keeps transcript rows so the preview still reflects the last bubble; clear-history / reset remove rows so the preview becomes the wipe confirm (or `null` after a full unbounded wipe with no remaining user).
182
193
  - No registered project → clear `project_required` (ask the user to configure a project). Never fall back to managed assistant-workspaces.
183
194
  - Assign must not go through the peer DM channel.
184
195
  - `assignWork` (service) resolves the worker model via the connected catalog (`capabilities.input.image` preferred), forwards tool `variant` (cross-model drops contact variant), passes AbortSignal into create/prompt/delete/catalog, and supplies `lookupMessage` over real SDK exact lookup (`v2.session.message` → `session.message` → bounded `message.list`). Ambiguous prompt admission returns `prompt_ambiguous` with `sessionID`/`messageID` and does **not** delete the worker; the contact-turn assign gate retains that identity so a same-args retry reuses the session and cannot blind-create another worker this turn.
@@ -256,7 +256,7 @@ describe('contact attachments real HTTP (core-routes + assistants routes)', () =
256
256
  });
257
257
 
258
258
  describe('contact attachment materialize failure terminal settle', () => {
259
- it('deleting bytes after admit causes durable error, working=false, next turn ok', async () => {
259
+ it('deleting bytes after admit causes transient error, working=false, next turn ok', async () => {
260
260
  const directory = mkRoot();
261
261
  closers.push(async () => fsp.rm(directory, { recursive: true, force: true }).catch(() => {}));
262
262
 
@@ -339,7 +339,7 @@ describe('contact attachment materialize failure terminal settle', () => {
339
339
  expect(snap.activeContactTurn).toBeNull();
340
340
 
341
341
  const page = service.contactMessages(assistant.id, { limit: 20 });
342
- expect(page.messages.some((m) => m.status === 'error' || (m.role === 'assistant' && m.text))).toBe(true);
342
+ expect(page.messages.some((m) => m.status === 'error' || (m.role === 'assistant' && m.text))).toBe(false);
343
343
  const pageJson = JSON.stringify(page);
344
344
  expect(pageJson).not.toMatch(/data:image\/png;base64,[A-Za-z0-9+/]{40,}/);
345
345
 
@@ -107,6 +107,15 @@ export function ensureContactSchema(db) {
107
107
  // only counts assistant tails that belong to this delivery.
108
108
  if (!watchColumns.has('message_id')) db.exec('ALTER TABLE assistant_contact_watch ADD COLUMN message_id TEXT');
109
109
  // Attachment table ownership lives in contact-attachments.ensureContactAttachmentSchema.
110
+ // Errors are live feedback, not history. Remove legacy rows and their bodies atomically.
111
+ db.transaction(() => {
112
+ db.exec(`
113
+ DELETE FROM assistant_contact_part WHERE message_id IN (
114
+ SELECT message_id FROM assistant_contact_message WHERE status = 'error'
115
+ );
116
+ DELETE FROM assistant_contact_message WHERE status = 'error';
117
+ `);
118
+ })();
110
119
  }
111
120
 
112
121
  /** Current contact transcript generation for one assistant (default 0). */
@@ -719,7 +728,7 @@ export function sanitizeContactPreviewText(text, maxChars = CONTACT_PREVIEW_MAX_
719
728
  * Build AssistantDTO.latestMessagePreview from one hydrated contact message.
720
729
  * Returns null when the row is not list-visible (internal settle-only, empty,
721
730
  * unsupported role). Peer inbox rows map to role `user` (user-visible DM).
722
- * Visible error bubbles keep real body text. Cards/files set fallbackKind.
731
+ * Cards/files set fallbackKind. Error feedback never enters persisted previews.
723
732
  */
724
733
  export function buildContactMessagePreview(message) {
725
734
  if (!message || typeof message !== 'object') return null;
@@ -1003,6 +1012,7 @@ export function insertContactMessage(db, {
1003
1012
  fromAssistantID = null,
1004
1013
  fromAssistantName = null,
1005
1014
  }) {
1015
+ if (status === 'error') return;
1006
1016
  db.prepare(
1007
1017
  'INSERT INTO assistant_contact_message(message_id,assistant_id,role,turn_id,bubble_index,created_at,ordinal,status,from_assistant_id,from_assistant_name) VALUES (?,?,?,?,?,?,?,?,?,?)',
1008
1018
  ).run(messageID, assistantID, role, turnID, bubbleIndex, createdAt, ordinal, status, fromAssistantID, fromAssistantName);
@@ -120,7 +120,7 @@ describe('contact LLM history trim', () => {
120
120
  { role: 'assistant', content: 'keep-assistant' },
121
121
  { role: 'assistant', content: expect.stringContaining('\"sessionID\":\"ses_1\"') },
122
122
  ]);
123
- expect(listContactMessages(db, assistantID, { limit: 50 }).messages).toHaveLength(5);
123
+ expect(listContactMessages(db, assistantID, { limit: 50 }).messages).toHaveLength(4);
124
124
  db.close();
125
125
  });
126
126
 
@@ -549,21 +549,40 @@ describe('contact latest message preview', () => {
549
549
  db.close();
550
550
  });
551
551
 
552
- it('keeps durable error body text as the preview', () => {
552
+ it('never stores error rows or replaces the latest message preview with an error', () => {
553
553
  const db = openDb();
554
554
  const assistantID = 'asst_preview_err';
555
- insert(db, assistantID, { role: 'user', text: 'hi' });
555
+ const user = insert(db, assistantID, { role: 'user', text: 'hi' });
556
556
  const err = insert(db, assistantID, {
557
557
  role: 'assistant',
558
558
  text: 'No connected model',
559
559
  status: 'error',
560
560
  });
561
561
  expect(getLatestContactMessagePreview(db, assistantID)).toMatchObject({
562
- messageID: err.messageID,
563
- role: 'assistant',
564
- text: 'No connected model',
562
+ messageID: user.messageID,
563
+ role: 'user',
564
+ text: 'hi',
565
565
  fallbackKind: null,
566
566
  });
567
+ expect(db.prepare('SELECT * FROM assistant_contact_message WHERE message_id=?').get(err.messageID)).toBeUndefined();
568
+ expect(db.prepare('SELECT * FROM assistant_contact_part WHERE message_id=?').all(err.messageID)).toEqual([]);
569
+ db.close();
570
+ });
571
+
572
+ it('atomically purges legacy error messages and parts on startup', () => {
573
+ const db = openDb();
574
+ const user = insert(db, 'legacy', { role: 'user', text: 'keep' });
575
+ const legacy = insert(db, 'legacy', { role: 'assistant', text: 'old failure' });
576
+ db.prepare("UPDATE assistant_contact_message SET status='error' WHERE message_id=?").run(legacy.messageID);
577
+ db.exec(`CREATE TRIGGER fail_error_cleanup BEFORE DELETE ON assistant_contact_message
578
+ WHEN OLD.status = 'error' BEGIN SELECT RAISE(ABORT, 'cleanup failed'); END;`);
579
+ expect(() => ensureContactSchema(db)).toThrow('cleanup failed');
580
+ expect(db.prepare('SELECT * FROM assistant_contact_part WHERE message_id=?').all(legacy.messageID)).toHaveLength(1);
581
+ db.exec('DROP TRIGGER fail_error_cleanup');
582
+ ensureContactSchema(db);
583
+ ensureContactSchema(db);
584
+ expect(listContactMessages(db, 'legacy').messages.map((message) => message.messageID)).toEqual([user.messageID]);
585
+ expect(db.prepare('SELECT * FROM assistant_contact_part WHERE message_id=?').all(legacy.messageID)).toEqual([]);
567
586
  db.close();
568
587
  });
569
588
 
@@ -718,7 +737,7 @@ describe('contact latest message preview', () => {
718
737
  });
719
738
 
720
739
  describe('contact read watermark / unread', () => {
721
- it('counts only complete/error assistant and peer replies; ignores user and settle-only', () => {
740
+ it('counts complete assistant and peer replies; ignores errors, user and settle-only', () => {
722
741
  expect(isContactVisiblePart({ type: 'text', text: 'hi' })).toBe(true);
723
742
  expect(isContactVisiblePart({ type: 'text', text: ' ' })).toBe(false);
724
743
  expect(isContactVisiblePart({ type: 'text', text: CONTACT_SETTLE_TEXT.complete })).toBe(false);
@@ -793,7 +812,7 @@ describe('contact read watermark / unread', () => {
793
812
  insert(db, assistantID, { role: 'peer', text: 'p1', fromAssistantID: 'other', fromAssistantName: 'Peer' });
794
813
  insert(db, assistantID, { role: 'assistant', text: 'failed', status: 'error' });
795
814
  // No watermark → all countable unread.
796
- expect(countContactUnread(db, assistantID)).toBe(3);
815
+ expect(countContactUnread(db, assistantID)).toBe(2);
797
816
  db.close();
798
817
  });
799
818
 
@@ -1423,31 +1423,6 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
1423
1423
  throw error;
1424
1424
  }
1425
1425
  };
1426
- /** Durable failure bubble so APP restart can recover error without SSE. */
1427
- const persistContactTurnFailure = (assistantID, { userMessageID, turnID, error }) => {
1428
- const detail = typeof error === 'string' && error.trim() ? error.trim() : 'upstream_error';
1429
- const failureID = `${userMessageID}:error`;
1430
- if (getContactMessage(db, failureID)) return;
1431
- db.exec('BEGIN IMMEDIATE');
1432
- try {
1433
- insertContactMessage(db, {
1434
- messageID: failureID,
1435
- assistantID,
1436
- role: 'assistant',
1437
- turnID,
1438
- bubbleIndex: 0,
1439
- createdAt: now(),
1440
- ordinal: nextContactOrdinal(db, assistantID),
1441
- status: 'error',
1442
- parts: [{ type: 'text', text: detail }],
1443
- });
1444
- bump();
1445
- db.exec('COMMIT');
1446
- } catch (writeError) {
1447
- db.exec('ROLLBACK');
1448
- throw writeError;
1449
- }
1450
- };
1451
1426
  const contactBubbleMessageID = (userMessageID, bubbleIndex) => `${userMessageID}:bubble:${bubbleIndex + 1}`;
1452
1427
  const contactCardMessageID = (userMessageID, index) => `${userMessageID}:card:${index + 1}`;
1453
1428
 
@@ -2695,7 +2670,7 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
2695
2670
  const messageID = scheduledRun ? `scheduled_${assistantID}_${scheduledRun.id}` : assignedSessionResumeMessageID(assistantID, sessionID, status, updatedAt);
2696
2671
  const capturedAssistant = scheduledRun ? output(row) : null;
2697
2672
  const turnID = messageID;
2698
- // Idempotent: same watch status already resumed (bubbles or durable error).
2673
+ // Idempotent: same watch status already resumed with published output.
2699
2674
  if (
2700
2675
  getContactMessage(db, `${messageID}:bubble:1`)
2701
2676
  || getContactMessage(db, `${messageID}:error`)
@@ -2966,16 +2941,7 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
2966
2941
  ].filter((card, index, list) => list.findIndex((item) => contactCardIdentity(item) === contactCardIdentity(card)) === index);
2967
2942
  if (bubbles.length === 0 && cards.length === 0) {
2968
2943
  const detail = 'Assistant returned no text';
2969
- try {
2970
- persistContactTurnFailure(assistantID, {
2971
- userMessageID: messageID,
2972
- turnID,
2973
- error: detail,
2974
- });
2975
- settleActiveContactTurn(assistantID, turnID);
2976
- } catch {
2977
- settleActiveContactTurn(assistantID, turnID, { bumpRevision: true });
2978
- }
2944
+ settleActiveContactTurn(assistantID, turnID, { bumpRevision: true });
2979
2945
  emitContactTurnEvent('openchamber:contact-turn-end', {
2980
2946
  assistantID,
2981
2947
  turnID,
@@ -3021,16 +2987,7 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
3021
2987
  ? error.message.trim()
3022
2988
  : (error?.code || 'upstream_error');
3023
2989
  const statusCode = error?.code === 'no_provider' ? 'no_provider' : (error?.code || 'upstream_error');
3024
- try {
3025
- persistContactTurnFailure(assistantID, {
3026
- userMessageID: messageID,
3027
- turnID,
3028
- error: detail,
3029
- });
3030
- settleActiveContactTurn(assistantID, turnID);
3031
- } catch {
3032
- settleActiveContactTurn(assistantID, turnID, { bumpRevision: true });
3033
- }
2990
+ settleActiveContactTurn(assistantID, turnID, { bumpRevision: true });
3034
2991
  emitContactTurnEvent('openchamber:contact-turn-end', {
3035
2992
  assistantID,
3036
2993
  turnID,
@@ -3182,7 +3139,7 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
3182
3139
  };
3183
3140
  const modelContext = await loadContactModelContext(controller.signal);
3184
3141
  try {
3185
- // Materialize inside turn try so failures durable-error + settle working.
3142
+ // Materialize inside turn try so failures emit live feedback and settle working.
3186
3143
  // DB/UI keep descriptors; harness + assign get execution-time data URLs under budget.
3187
3144
  const executionHistory = await materializeContactHistory(
3188
3145
  db,
@@ -3340,16 +3297,7 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
3340
3297
  ].filter((card, index, list) => list.findIndex((item) => contactCardIdentity(item) === contactCardIdentity(card)) === index);
3341
3298
  if (bubbles.length === 0 && cards.length === 0) {
3342
3299
  const detail = 'Assistant returned no text';
3343
- try {
3344
- persistContactTurnFailure(row.assistant_id, {
3345
- userMessageID: messageID,
3346
- turnID,
3347
- error: detail,
3348
- });
3349
- settleActiveContactTurn(row.assistant_id, turnID);
3350
- } catch {
3351
- settleActiveContactTurn(row.assistant_id, turnID, { bumpRevision: true });
3352
- }
3300
+ settleActiveContactTurn(row.assistant_id, turnID, { bumpRevision: true });
3353
3301
  emitContactTurnEvent('openchamber:contact-turn-end', {
3354
3302
  assistantID: row.assistant_id,
3355
3303
  turnID,
@@ -3409,16 +3357,7 @@ export const createAssistantsService = ({ dbPath, dataDir, buildOpenCodeUrl, get
3409
3357
  ? error.message.trim()
3410
3358
  : (error?.code || 'upstream_error');
3411
3359
  const statusCode = error?.code === 'no_provider' ? 'no_provider' : (error?.code || 'upstream_error');
3412
- try {
3413
- persistContactTurnFailure(row.assistant_id, {
3414
- userMessageID: messageID,
3415
- turnID,
3416
- error: detail,
3417
- });
3418
- settleActiveContactTurn(row.assistant_id, turnID);
3419
- } catch {
3420
- settleActiveContactTurn(row.assistant_id, turnID, { bumpRevision: true });
3421
- }
3360
+ settleActiveContactTurn(row.assistant_id, turnID, { bumpRevision: true });
3422
3361
  emitContactTurnEvent('openchamber:contact-turn-end', {
3423
3362
  assistantID: row.assistant_id,
3424
3363
  turnID,
@@ -4230,8 +4230,11 @@ describe('assistants service', () => {
4230
4230
  service.close();
4231
4231
  });
4232
4232
 
4233
- it('persists a durable contact error bubble so query recovery works without SSE', async () => {
4234
- const service = setup(root(), {}, {
4233
+ it('delivers contact errors without persisting a transcript message', async () => {
4234
+ const directory = root();
4235
+ const events = [];
4236
+ const service = setup(directory, {}, {
4237
+ onContactTurnEvent: (event) => events.push(event),
4235
4238
  runContactTurn: async () => {
4236
4239
  const error = new Error('No connected model');
4237
4240
  error.code = 'no_provider';
@@ -4247,10 +4250,23 @@ describe('assistants service', () => {
4247
4250
  const page = service.contactMessages(assistant.id);
4248
4251
  expect(page.messages.map((message) => ({ role: message.role, status: message.status, text: message.text }))).toEqual([
4249
4252
  { role: 'user', status: 'complete', text: 'hi' },
4250
- { role: 'assistant', status: 'error', text: 'No connected model' },
4251
4253
  ]);
4252
4254
  expect(service.snapshot().assistants[0]).toMatchObject({ working: false, activeContactTurn: null });
4255
+ expect(events).toContainEqual(expect.objectContaining({
4256
+ type: 'openchamber:contact-turn-end',
4257
+ properties: expect.objectContaining({ status: 'error', error: 'No connected model' }),
4258
+ }));
4253
4259
  service.close();
4260
+ const restarted = setup(directory, {}, {
4261
+ runContactTurn: async () => ({ text: 'ok', bubbles: ['ok'] }),
4262
+ });
4263
+ expect(restarted.contactMessages(assistant.id).messages.map((message) => message.text)).toEqual(['hi']);
4264
+ await restarted.send(assistant.id, {
4265
+ messageID: 'after_failure', parts: [{ type: 'text', text: 'again' }],
4266
+ });
4267
+ await restarted.whenContactTurnSettled('after_failure');
4268
+ expect(restarted.contactMessages(assistant.id).messages.map((message) => message.text)).toEqual(['hi', 'again', 'ok']);
4269
+ restarted.close();
4254
4270
  });
4255
4271
 
4256
4272
  it('bumps revision when queued contact turn becomes running', async () => {
@@ -4395,10 +4411,9 @@ describe('assistants service', () => {
4395
4411
  && event.properties?.status === 'error'
4396
4412
  && event.properties?.turnID === 'client_no_provider'
4397
4413
  ))).toBe(true);
4398
- // User row stays; durable error bubble enables query recovery without SSE.
4414
+ // User row stays; error feedback is delivered only through the live event.
4399
4415
  expect(service.contactMessages(assistant.id).messages.map((message) => ({ role: message.role, status: message.status }))).toEqual([
4400
4416
  { role: 'user', status: 'complete' },
4401
- { role: 'assistant', status: 'error' },
4402
4417
  ]);
4403
4418
  service.close();
4404
4419
  });