@shawnstack/quickforge 1.7.10 → 1.7.11

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/{AgentProfilesPage-Dw9orPNt.js → AgentProfilesPage-Cvb1KTz5.js} +1 -1
  2. package/dist/assets/ChatPanelHost-C7zV-xE_.js +48 -0
  3. package/dist/assets/{CloudAccountSettingsPage-CDkKN1Sz.js → CloudAccountSettingsPage-mAs_nbvw.js} +1 -1
  4. package/dist/assets/{PluginsPage-Dj1bmsEf.js → PluginsPage-mLvwvTmL.js} +1 -1
  5. package/dist/assets/{ScheduledTasksPage-C9lOTi75.js → ScheduledTasksPage-CuUW_9lq.js} +1 -1
  6. package/dist/assets/{SettingsWorkspacePage-CFuvSATi.js → SettingsWorkspacePage-0OINfEqK.js} +390 -390
  7. package/dist/assets/{ShareLinksSettingsPage-CH_t33A3.js → ShareLinksSettingsPage-DqeyJYoD.js} +1 -1
  8. package/dist/assets/{SharedConversationPage-DLo05j7c.js → SharedConversationPage-C7CDEoX9.js} +1 -1
  9. package/dist/assets/TerminalDock-CZXcuMpq.js +2 -0
  10. package/dist/assets/{WorkspaceInspector-DPXgLIum.js → WorkspaceInspector-CNSBOEvP.js} +3 -3
  11. package/dist/assets/{index-DQBOm9gt.js → index-BgMvQW1b.js} +13 -13
  12. package/dist/assets/index-CB1g2D0f.css +3 -0
  13. package/dist/assets/local-tools-Dopjg9xq.js +270 -0
  14. package/dist/assets/{mcp-servers-dialog-CwHp0K4q.js → mcp-servers-dialog-B2lALtTE.js} +1 -1
  15. package/dist/assets/{skills-dialog-k9ycekIU.js → skills-dialog-DYp35JM3.js} +1 -1
  16. package/dist/index.html +4 -4
  17. package/package.json +1 -1
  18. package/server/acp/server.mjs +3 -6
  19. package/server/agent-manager.mjs +62 -46
  20. package/server/auto-archive.mjs +55 -8
  21. package/server/index.mjs +172 -51
  22. package/server/lan-access-cutover.mjs +21 -4
  23. package/server/maintenance/downgrade-session-state-v1.mjs +46 -85
  24. package/server/maintenance/export-session-state-v1.mjs +1 -11
  25. package/server/routes/backup.mjs +1 -1
  26. package/server/routes/storage.mjs +63 -49
  27. package/server/scheduled-runs-cutover.mjs +15 -1
  28. package/server/session-index-service.mjs +57 -391
  29. package/server/session-state-backup.mjs +56 -5
  30. package/server/session-state-import.mjs +157 -0
  31. package/server/session-state-maintenance.mjs +126 -0
  32. package/server/session-state-service.mjs +128 -210
  33. package/server/share-cutover.mjs +21 -4
  34. package/server/sqlite/database.mjs +142 -7
  35. package/server/sqlite/lan-access-repository.mjs +6 -4
  36. package/server/sqlite/migrations.mjs +109 -0
  37. package/server/sqlite/scheduled-task-runs-repository.mjs +6 -3
  38. package/server/sqlite/session-index-repository.mjs +59 -208
  39. package/server/sqlite/session-state-repository.mjs +337 -387
  40. package/server/sqlite/share-repository.mjs +56 -45
  41. package/server/startup-state.mjs +107 -0
  42. package/server/storage.mjs +218 -454
  43. package/server/utils/logger.mjs +15 -4
  44. package/dist/assets/ChatPanelHost-CW3-MQyL.js +0 -48
  45. package/dist/assets/TerminalDock-CSOVub-m.js +0 -2
  46. package/dist/assets/index-ps85guTs.css +0 -3
  47. package/dist/assets/local-tools-BxfQeIUQ.js +0 -270
  48. package/server/session-state-cutover.mjs +0 -563
@@ -1,3 +1,3 @@
1
- import{i as e,n as t}from"./rolldown-runtime-DWdDZTNf.js";import{Bt as n,Gt as r,J as i,Jt as a,b as o,n as s}from"./icons-DM_qJ7VJ.js";import{n as c}from"./react-vendor-BpWW_Nvp.js";import{An as l,Dn as u,Pn as d}from"./index-DQBOm9gt.js";var f=t({SkillsDialog:()=>_,SkillsManagerPanel:()=>g}),p=e(a(),1),m=c();async function h(e){let t=await e.json().catch(()=>null);if(!e.ok)throw Error(t?.error||`HTTP ${e.status}`);return t}function g({active:e=!0,scope:t,project:a,embedded:c=!1,onClose:f,onSaved:g}){let[_,v]=(0,p.useState)([]),[y,b]=(0,p.useState)(()=>new Set),[x,S]=(0,p.useState)(``),[C,w]=(0,p.useState)([]),[T,E]=(0,p.useState)(!1),[D,O]=(0,p.useState)(!1),[k,A]=(0,p.useState)(``),j=t===`project`,[M,N]=(0,p.useState)(null),[P,F]=(0,p.useState)(null),[I,L]=(0,p.useState)(!1),[R,z]=(0,p.useState)(``),B=(0,p.useCallback)(()=>{N(null),F(null),z(``)},[]),V=(0,p.useCallback)(()=>{B(),f?.()},[f,B]);(0,p.useEffect)(()=>{if(!e||j&&!a)return;let t=!1;return(async()=>{E(!0),A(``);try{let e=j?`/api/skills?projectId=${encodeURIComponent(a.id)}`:`/api/skills?scope=global`,n=await h(await fetch(e));if(t)return;v(Array.isArray(n.skills)?n.skills:[]),b(new Set(Array.isArray(n.selectedSkills)?n.selectedSkills:[])),w(Array.isArray(n.searchPaths)?n.searchPaths:[])}catch(e){t||A(e instanceof Error?e.message:d(`failedToLoadSkills`))}finally{t||E(!1)}})(),()=>{t=!0}},[e,a,j]),(0,p.useEffect)(()=>{if(!e||c)return;let t=e=>{e.key===`Escape`&&(M?B():D||V())};return document.addEventListener(`keydown`,t),()=>document.removeEventListener(`keydown`,t)},[e,V,c,D,M,B]);let H=(0,p.useMemo)(()=>{let e=x.trim().toLowerCase();return e?_.filter(t=>[t.name,t.displayName,t.description,t.source,t.compatibility,t.allowedTools,...t.tags??[],...t.triggers??[]].filter(Boolean).join(` `).toLowerCase().includes(e)):_},[x,_]),U=async e=>{N(e),F(null),z(``),L(!0);try{let t=new URLSearchParams({name:e});j&&a?(t.set(`scope`,`project`),t.set(`projectId`,a.id)):t.set(`scope`,`global`),F(await h(await fetch(`/api/skills/content?${t}`)))}catch(e){z(e instanceof Error?e.message:d(`failedToReadSkill`))}finally{L(!1)}};if(!e||j&&!a)return null;let W=e=>{if(D)return;let t=new Set(y);t.has(e)?t.delete(e):t.add(e),b(t),G(t)},G=async e=>{O(!0),A(``);try{let n=await h(await fetch(j?`/api/skills/project`:`/api/skills/global`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify(j?{projectId:a.id,selectedSkills:[...e]}:{selectedSkills:[...e]})}));g({scope:t,project:j?n.projects?.find(e=>e.id===a.id)??{...a,skills:n.selectedSkills}:void 0,projects:n.projects,selectedSkills:n.selectedSkills})}catch(e){b(y),A(e instanceof Error?e.message:d(`failedToSaveSkills`))}finally{O(!1)}},K=d(j?`projectSkills`:`globalSkills`),q=j?d(`projectSkillsDescription`,{project:a.name}):d(`globalSkillsDescription`),J=(0,m.jsxs)(m.Fragment,{children:[(0,m.jsx)(`div`,{className:`quickforge-settings-divider p-3`,children:(0,m.jsxs)(`div`,{className:`quickforge-settings-inline-field`,children:[(0,m.jsx)(o,{className:`pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground/60`}),(0,m.jsx)(`input`,{value:x,onChange:e=>S(e.target.value),placeholder:d(`searchSkills`),className:`quickforge-settings-input`,disabled:T||D})]})}),k?(0,m.jsx)(`div`,{className:`quickforge-settings-alert quickforge-settings-warning-attached`,children:k}):null,(0,m.jsxs)(`div`,{className:`quickforge-settings-toolbar`,children:[(0,m.jsx)(`span`,{className:`quickforge-settings-row-title`,children:d(`availableSkills`)}),(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-info`,children:d(`selectedSkillsCount`,{count:y.size})})]}),(0,m.jsx)(`div`,{className:c?``:`max-h-[46vh] overflow-y-auto`,children:T?(0,m.jsxs)(`div`,{className:`quickforge-settings-empty-row inline-flex items-center gap-2`,children:[(0,m.jsx)(i,{className:`size-4 animate-spin`}),d(`loading`)]}):H.length===0?(0,m.jsx)(`div`,{className:`quickforge-settings-empty-row`,children:d(`noMatchingSkills`)}):H.map(e=>{let t=y.has(e.name);return(0,m.jsxs)(`div`,{className:l(`quickforge-settings-list-item`,t&&`bg-muted/12`),children:[(0,m.jsxs)(`div`,{className:`quickforge-settings-list-item-main`,children:[(0,m.jsx)(`div`,{className:`quickforge-settings-row-title`,children:e.displayName||e.name}),e.description?(0,m.jsx)(`div`,{className:`quickforge-settings-row-description`,children:e.description}):null,(0,m.jsxs)(`div`,{className:`quickforge-settings-meta`,children:[e.source?(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:e.source}):null,e.compatibility?(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:e.compatibility}):null,e.allowedTools?(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:e.allowedTools}):null,e.tags?.slice(0,5).map(e=>(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:e},e))]})]}),(0,m.jsxs)(`div`,{className:`quickforge-settings-list-item-actions`,onClick:e=>e.stopPropagation(),children:[(0,m.jsxs)(`label`,{className:`quickforge-settings-switch`,"aria-disabled":D?`true`:`false`,children:[(0,m.jsx)(`input`,{type:`checkbox`,checked:t,disabled:D,onChange:()=>W(e.name),"aria-label":t?`Disable ${e.name}`:`Enable ${e.name}`}),(0,m.jsx)(`span`,{"aria-hidden":`true`})]}),(0,m.jsx)(`button`,{type:`button`,className:`quickforge-settings-icon-action`,onClick:()=>void U(e.name),disabled:D||I,title:d(`readSkill`),"aria-label":`${d(`readSkill`)}: ${e.displayName||e.name}`,children:(0,m.jsx)(n,{className:`size-4`})})]})]},e.name)})})]}),Y=(0,m.jsxs)(m.Fragment,{children:[(0,m.jsxs)(`div`,{className:`quickforge-settings-toolbar`,children:[(0,m.jsxs)(`button`,{type:`button`,className:`quickforge-settings-button quickforge-settings-button-secondary`,onClick:B,disabled:I,children:[(0,m.jsx)(r,{className:`mr-2 size-4`}),d(`backToSkillList`)]}),(0,m.jsx)(`div`,{className:`quickforge-settings-row-main min-w-0`,children:(0,m.jsx)(`div`,{className:`quickforge-settings-row-title truncate`,children:P?.displayName||P?.name||M})})]}),(0,m.jsx)(`div`,{className:`p-4`,children:I?(0,m.jsxs)(`div`,{className:`quickforge-settings-empty-row inline-flex items-center gap-2`,children:[(0,m.jsx)(i,{className:`size-4 animate-spin`}),d(`loading`)]}):R?(0,m.jsx)(`div`,{className:`quickforge-settings-alert quickforge-settings-warning-attached`,children:R}):P?(0,m.jsxs)(m.Fragment,{children:[P.description?(0,m.jsx)(`div`,{className:`quickforge-settings-row-description mb-3`,children:P.description}):null,(0,m.jsxs)(`div`,{className:`quickforge-settings-meta mb-3`,children:[P.version?(0,m.jsxs)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:[`v`,P.version]}):null,P.source?(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:P.source}):null,P.compatibility?(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:P.compatibility}):null,P.allowedTools?(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:P.allowedTools}):null,P.license?(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:P.license}):null,P.tags?.map(e=>(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:e},e))]}),P.triggers?.length?(0,m.jsxs)(`div`,{className:`quickforge-settings-row-description mb-3`,children:[`Triggers: `,P.triggers.join(`, `)]}):null,P.instructions?(0,m.jsx)(`pre`,{className:`whitespace-pre-wrap break-words rounded-md border border-border bg-muted/30 px-4 py-3 font-mono text-sm leading-6 text-foreground/80`,children:P.instructions}):(0,m.jsx)(`div`,{className:`quickforge-settings-empty-row`,children:d(`noSkillContent`)})]}):null})]});return c?(0,m.jsx)(`div`,{className:`quickforge-settings-stack`,children:(0,m.jsx)(`section`,{className:`quickforge-settings-section`,"aria-label":K,children:M?Y:(0,m.jsxs)(m.Fragment,{children:[C.length?(0,m.jsx)(`div`,{className:`px-5 pt-4`,children:(0,m.jsxs)(`span`,{className:`quickforge-settings-row-description inline-flex items-center`,children:[d(`skillSearchPaths`),(0,m.jsx)(u,{label:C.join(`
1
+ import{i as e,n as t}from"./rolldown-runtime-DWdDZTNf.js";import{Bt as n,Gt as r,J as i,Jt as a,b as o,n as s}from"./icons-DM_qJ7VJ.js";import{n as c}from"./react-vendor-BpWW_Nvp.js";import{An as l,Dn as u,Pn as d}from"./index-BgMvQW1b.js";var f=t({SkillsDialog:()=>_,SkillsManagerPanel:()=>g}),p=e(a(),1),m=c();async function h(e){let t=await e.json().catch(()=>null);if(!e.ok)throw Error(t?.error||`HTTP ${e.status}`);return t}function g({active:e=!0,scope:t,project:a,embedded:c=!1,onClose:f,onSaved:g}){let[_,v]=(0,p.useState)([]),[y,b]=(0,p.useState)(()=>new Set),[x,S]=(0,p.useState)(``),[C,w]=(0,p.useState)([]),[T,E]=(0,p.useState)(!1),[D,O]=(0,p.useState)(!1),[k,A]=(0,p.useState)(``),j=t===`project`,[M,N]=(0,p.useState)(null),[P,F]=(0,p.useState)(null),[I,L]=(0,p.useState)(!1),[R,z]=(0,p.useState)(``),B=(0,p.useCallback)(()=>{N(null),F(null),z(``)},[]),V=(0,p.useCallback)(()=>{B(),f?.()},[f,B]);(0,p.useEffect)(()=>{if(!e||j&&!a)return;let t=!1;return(async()=>{E(!0),A(``);try{let e=j?`/api/skills?projectId=${encodeURIComponent(a.id)}`:`/api/skills?scope=global`,n=await h(await fetch(e));if(t)return;v(Array.isArray(n.skills)?n.skills:[]),b(new Set(Array.isArray(n.selectedSkills)?n.selectedSkills:[])),w(Array.isArray(n.searchPaths)?n.searchPaths:[])}catch(e){t||A(e instanceof Error?e.message:d(`failedToLoadSkills`))}finally{t||E(!1)}})(),()=>{t=!0}},[e,a,j]),(0,p.useEffect)(()=>{if(!e||c)return;let t=e=>{e.key===`Escape`&&(M?B():D||V())};return document.addEventListener(`keydown`,t),()=>document.removeEventListener(`keydown`,t)},[e,V,c,D,M,B]);let H=(0,p.useMemo)(()=>{let e=x.trim().toLowerCase();return e?_.filter(t=>[t.name,t.displayName,t.description,t.source,t.compatibility,t.allowedTools,...t.tags??[],...t.triggers??[]].filter(Boolean).join(` `).toLowerCase().includes(e)):_},[x,_]),U=async e=>{N(e),F(null),z(``),L(!0);try{let t=new URLSearchParams({name:e});j&&a?(t.set(`scope`,`project`),t.set(`projectId`,a.id)):t.set(`scope`,`global`),F(await h(await fetch(`/api/skills/content?${t}`)))}catch(e){z(e instanceof Error?e.message:d(`failedToReadSkill`))}finally{L(!1)}};if(!e||j&&!a)return null;let W=e=>{if(D)return;let t=new Set(y);t.has(e)?t.delete(e):t.add(e),b(t),G(t)},G=async e=>{O(!0),A(``);try{let n=await h(await fetch(j?`/api/skills/project`:`/api/skills/global`,{method:`PUT`,headers:{"content-type":`application/json`},body:JSON.stringify(j?{projectId:a.id,selectedSkills:[...e]}:{selectedSkills:[...e]})}));g({scope:t,project:j?n.projects?.find(e=>e.id===a.id)??{...a,skills:n.selectedSkills}:void 0,projects:n.projects,selectedSkills:n.selectedSkills})}catch(e){b(y),A(e instanceof Error?e.message:d(`failedToSaveSkills`))}finally{O(!1)}},K=d(j?`projectSkills`:`globalSkills`),q=j?d(`projectSkillsDescription`,{project:a.name}):d(`globalSkillsDescription`),J=(0,m.jsxs)(m.Fragment,{children:[(0,m.jsx)(`div`,{className:`quickforge-settings-divider p-3`,children:(0,m.jsxs)(`div`,{className:`quickforge-settings-inline-field`,children:[(0,m.jsx)(o,{className:`pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground/60`}),(0,m.jsx)(`input`,{value:x,onChange:e=>S(e.target.value),placeholder:d(`searchSkills`),className:`quickforge-settings-input`,disabled:T||D})]})}),k?(0,m.jsx)(`div`,{className:`quickforge-settings-alert quickforge-settings-warning-attached`,children:k}):null,(0,m.jsxs)(`div`,{className:`quickforge-settings-toolbar`,children:[(0,m.jsx)(`span`,{className:`quickforge-settings-row-title`,children:d(`availableSkills`)}),(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-info`,children:d(`selectedSkillsCount`,{count:y.size})})]}),(0,m.jsx)(`div`,{className:c?``:`max-h-[46vh] overflow-y-auto`,children:T?(0,m.jsxs)(`div`,{className:`quickforge-settings-empty-row inline-flex items-center gap-2`,children:[(0,m.jsx)(i,{className:`size-4 animate-spin`}),d(`loading`)]}):H.length===0?(0,m.jsx)(`div`,{className:`quickforge-settings-empty-row`,children:d(`noMatchingSkills`)}):H.map(e=>{let t=y.has(e.name);return(0,m.jsxs)(`div`,{className:l(`quickforge-settings-list-item`,t&&`bg-muted/12`),children:[(0,m.jsxs)(`div`,{className:`quickforge-settings-list-item-main`,children:[(0,m.jsx)(`div`,{className:`quickforge-settings-row-title`,children:e.displayName||e.name}),e.description?(0,m.jsx)(`div`,{className:`quickforge-settings-row-description`,children:e.description}):null,(0,m.jsxs)(`div`,{className:`quickforge-settings-meta`,children:[e.source?(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:e.source}):null,e.compatibility?(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:e.compatibility}):null,e.allowedTools?(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:e.allowedTools}):null,e.tags?.slice(0,5).map(e=>(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:e},e))]})]}),(0,m.jsxs)(`div`,{className:`quickforge-settings-list-item-actions`,onClick:e=>e.stopPropagation(),children:[(0,m.jsxs)(`label`,{className:`quickforge-settings-switch`,"aria-disabled":D?`true`:`false`,children:[(0,m.jsx)(`input`,{type:`checkbox`,checked:t,disabled:D,onChange:()=>W(e.name),"aria-label":t?`Disable ${e.name}`:`Enable ${e.name}`}),(0,m.jsx)(`span`,{"aria-hidden":`true`})]}),(0,m.jsx)(`button`,{type:`button`,className:`quickforge-settings-icon-action`,onClick:()=>void U(e.name),disabled:D||I,title:d(`readSkill`),"aria-label":`${d(`readSkill`)}: ${e.displayName||e.name}`,children:(0,m.jsx)(n,{className:`size-4`})})]})]},e.name)})})]}),Y=(0,m.jsxs)(m.Fragment,{children:[(0,m.jsxs)(`div`,{className:`quickforge-settings-toolbar`,children:[(0,m.jsxs)(`button`,{type:`button`,className:`quickforge-settings-button quickforge-settings-button-secondary`,onClick:B,disabled:I,children:[(0,m.jsx)(r,{className:`mr-2 size-4`}),d(`backToSkillList`)]}),(0,m.jsx)(`div`,{className:`quickforge-settings-row-main min-w-0`,children:(0,m.jsx)(`div`,{className:`quickforge-settings-row-title truncate`,children:P?.displayName||P?.name||M})})]}),(0,m.jsx)(`div`,{className:`p-4`,children:I?(0,m.jsxs)(`div`,{className:`quickforge-settings-empty-row inline-flex items-center gap-2`,children:[(0,m.jsx)(i,{className:`size-4 animate-spin`}),d(`loading`)]}):R?(0,m.jsx)(`div`,{className:`quickforge-settings-alert quickforge-settings-warning-attached`,children:R}):P?(0,m.jsxs)(m.Fragment,{children:[P.description?(0,m.jsx)(`div`,{className:`quickforge-settings-row-description mb-3`,children:P.description}):null,(0,m.jsxs)(`div`,{className:`quickforge-settings-meta mb-3`,children:[P.version?(0,m.jsxs)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:[`v`,P.version]}):null,P.source?(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:P.source}):null,P.compatibility?(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:P.compatibility}):null,P.allowedTools?(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:P.allowedTools}):null,P.license?(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:P.license}):null,P.tags?.map(e=>(0,m.jsx)(`span`,{className:`quickforge-settings-badge quickforge-settings-badge-muted`,children:e},e))]}),P.triggers?.length?(0,m.jsxs)(`div`,{className:`quickforge-settings-row-description mb-3`,children:[`Triggers: `,P.triggers.join(`, `)]}):null,P.instructions?(0,m.jsx)(`pre`,{className:`whitespace-pre-wrap break-words rounded-md border border-border bg-muted/30 px-4 py-3 font-mono text-sm leading-6 text-foreground/80`,children:P.instructions}):(0,m.jsx)(`div`,{className:`quickforge-settings-empty-row`,children:d(`noSkillContent`)})]}):null})]});return c?(0,m.jsx)(`div`,{className:`quickforge-settings-stack`,children:(0,m.jsx)(`section`,{className:`quickforge-settings-section`,"aria-label":K,children:M?Y:(0,m.jsxs)(m.Fragment,{children:[C.length?(0,m.jsx)(`div`,{className:`px-5 pt-4`,children:(0,m.jsxs)(`span`,{className:`quickforge-settings-row-description inline-flex items-center`,children:[d(`skillSearchPaths`),(0,m.jsx)(u,{label:C.join(`
2
2
  `)})]})}):null,J]})})}):(0,m.jsx)(`div`,{className:`fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4`,onClick:e=>{e.target===e.currentTarget&&!D&&V()},children:(0,m.jsxs)(`div`,{className:`flex max-h-[85vh] w-full max-w-2xl flex-col rounded-lg border border-border bg-background shadow-quickforge`,children:[(0,m.jsxs)(`div`,{className:`quickforge-settings-toolbar`,children:[(0,m.jsxs)(`div`,{className:`quickforge-settings-row-main`,children:[(0,m.jsxs)(`div`,{className:`quickforge-settings-row-title`,children:[K,(0,m.jsx)(u,{label:q})]}),(0,m.jsx)(`div`,{className:`quickforge-settings-row-description`,children:q}),C.length?(0,m.jsxs)(`div`,{className:`quickforge-settings-row-description inline-flex items-center`,children:[d(`skillSearchPaths`),(0,m.jsx)(u,{label:C.join(`
3
3
  `)})]}):null]}),(0,m.jsx)(`button`,{type:`button`,className:`quickforge-settings-icon-action`,onClick:V,disabled:D,"aria-label":d(`close`),children:(0,m.jsx)(s,{className:`size-4`})})]}),(0,m.jsx)(`div`,{className:`min-h-0 flex-1 overflow-y-auto`,children:M?Y:J})]})})}function _({open:e,scope:t,project:n,onOpenChange:r,onSaved:i}){return(0,m.jsx)(g,{active:e,scope:t,project:n,onClose:()=>r(!1),onSaved:i})}export{f as n,_ as t};
package/dist/index.html CHANGED
@@ -11,7 +11,7 @@
11
11
  <meta name="apple-mobile-web-app-title" content="QuickForge" />
12
12
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
13
13
  <title>速构 QuickForge</title>
14
- <script type="module" crossorigin src="/assets/index-DQBOm9gt.js"></script>
14
+ <script type="module" crossorigin src="/assets/index-BgMvQW1b.js"></script>
15
15
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-DWdDZTNf.js">
16
16
  <link rel="modulepreload" crossorigin href="/assets/pi-ai-Cr2ee4a0.js">
17
17
  <link rel="modulepreload" crossorigin href="/assets/dist-yDHbi5ux.js">
@@ -22,9 +22,9 @@
22
22
  <link rel="modulepreload" crossorigin href="/assets/react-vendor-BpWW_Nvp.js">
23
23
  <link rel="modulepreload" crossorigin href="/assets/logger-8REuLPGK.js">
24
24
  <link rel="modulepreload" crossorigin href="/assets/model-display-label-CURwH0xM.js">
25
- <link rel="modulepreload" crossorigin href="/assets/index-DQBOm9gt.js">
26
- <link rel="modulepreload" crossorigin href="/assets/skills-dialog-k9ycekIU.js">
27
- <link rel="stylesheet" crossorigin href="/assets/index-ps85guTs.css">
25
+ <link rel="modulepreload" crossorigin href="/assets/index-BgMvQW1b.js">
26
+ <link rel="modulepreload" crossorigin href="/assets/skills-dialog-DYp35JM3.js">
27
+ <link rel="stylesheet" crossorigin href="/assets/index-CB1g2D0f.css">
28
28
  </head>
29
29
  <body>
30
30
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shawnstack/quickforge",
3
- "version": "1.7.10",
3
+ "version": "1.7.11",
4
4
  "description": "AI chat application with Agent access modes for local workspace tools. React + Vite + Tailwind CSS frontend, local Node.js storage server.",
5
5
  "keywords": [
6
6
  "ai",
@@ -922,16 +922,13 @@ export async function runQuickForgeAcpStdio() {
922
922
  console.log = (...args) => console.error(...args)
923
923
  let sqliteStorage
924
924
  try {
925
- const [{ ensureStorage, readPhysicalSessionMetadataBuckets, registerSessionMetadataCommitHook }, sqliteModule, sessionIndexModule] = await Promise.all([
925
+ const [storageModule, sqliteModule, sessionIndexModule] = await Promise.all([
926
926
  import('../storage.mjs'),
927
927
  import('../sqlite/database.mjs'),
928
928
  import('../session-index-service.mjs'),
929
929
  ])
930
- sqliteStorage = sqliteModule
931
- await ensureStorage()
932
- await sqliteModule.initializeSqliteStorage()
933
- sessionIndexModule.configureSessionIndex({ readBuckets: readPhysicalSessionMetadataBuckets })
934
- registerSessionMetadataCommitHook(sessionIndexModule.syncSessionMetadataCommit)
930
+ await storageModule.ensureStorage()
931
+ sqliteStorage = await sqliteModule.initializeSqliteStorage()
935
932
  await sessionIndexModule.initializeSessionIndex()
936
933
  const quickForgeAgent = await createQuickForgeAcpAgent()
937
934
  const stream = ndJsonStream(
@@ -28,8 +28,8 @@ import {
28
28
  validateModelReference,
29
29
  } from './agent-profile-schema.mjs'
30
30
  import { projectContextFromId, defaultGlobalWorkspaceContext, readProjectConfig } from './project-config.mjs'
31
- import { ensureStorage, readStore, atomicUpdate, atomicSessionMetadataUpdate, readSessionValue, writeSessionValue, deleteSessionValue, tempAgentsDir } from './storage.mjs'
32
- import { isSessionStateAuthoritative, readSessionStateRecord, saveSessionStatePair, deleteSessionState, storedMessagesState, sessionMessagesTailDigest } from './session-state-service.mjs'
31
+ import { ensureStorage, readStore, atomicUpdate, readSessionValue, tempAgentsDir } from './storage.mjs'
32
+ import { readSessionStateRecord, saveSessionStatePair, deleteSessionState, storedMessagesState, sessionMessagesTailDigest } from './session-state-service.mjs'
33
33
  import { logger } from './utils/logger.mjs'
34
34
  import { publishChannelSessionChanged } from './channels/event-relay.mjs'
35
35
  import { withSessionPersistenceLock } from './session-persistence-lock.mjs'
@@ -1967,6 +1967,10 @@ export async function createAgent(sessionId, config = {}) {
1967
1967
  persistedMessageCount: Number.isInteger(config.persistedMessageCount) && config.persistedMessageCount >= 0 ? config.persistedMessageCount : null,
1968
1968
  persistedTailDigest: typeof config.persistedTailDigest === 'string' ? config.persistedTailDigest : null,
1969
1969
  persistConflictCount: 0,
1970
+ // Set when an authoritative persist is skipped after CAS conflicts; cleared
1971
+ // on the next successful persist. Surfaced via getSessionState/getSessionStatus
1972
+ // so the UI can warn the user instead of silently losing messages.
1973
+ persistDegraded: null,
1970
1974
  lastAutoCompactAt: null,
1971
1975
  lastAutoCompactRejected: null,
1972
1976
  memoryEnabled: initialMemoryEnabled,
@@ -2153,6 +2157,26 @@ function canonicalStateJson(value) {
2153
2157
  return JSON.stringify(value)
2154
2158
  }
2155
2159
 
2160
+ /**
2161
+ * Mark the session as persist-degraded (an authoritative persist was skipped
2162
+ * after CAS conflicts) and notify connected clients so the UI can surface a
2163
+ * non-blocking warning instead of silently losing messages.
2164
+ */
2165
+ function markPersistDegraded(session, attempts) {
2166
+ session.persistDegraded = { at: new Date().toISOString(), attempts }
2167
+ emitSessionEvent(session, { type: 'persist_degraded', persistDegraded: true })
2168
+ }
2169
+
2170
+ /**
2171
+ * Clear the degraded marker after a successful persist and notify clients so
2172
+ * the UI warning disappears.
2173
+ */
2174
+ function clearPersistDegraded(session) {
2175
+ if (!session.persistDegraded) return
2176
+ session.persistDegraded = null
2177
+ emitSessionEvent(session, { type: 'persist_degraded', persistDegraded: false })
2178
+ }
2179
+
2156
2180
  /**
2157
2181
  * Persist a full session record in one authoritative SQLite transaction with
2158
2182
  * revision CAS. When the CAS fails because a concurrent metadata-only change
@@ -2201,6 +2225,7 @@ async function persistAuthoritativeSessionState(session, sessionData, metadata)
2201
2225
  session.persistedMessageCount = Array.isArray(sessionData.messages) ? sessionData.messages.length : 0
2202
2226
  }
2203
2227
  session.persistedTailDigest = sessionMessagesTailDigest(sessionData.messages)
2228
+ clearPersistDegraded(session)
2204
2229
  return { ...saved, metadata: mergedMetadata }
2205
2230
  } catch (error) {
2206
2231
  if (error?.errorCode !== 'SESSION_STATE_CONFLICT') throw error
@@ -2222,11 +2247,13 @@ async function persistAuthoritativeSessionState(session, sessionData, metadata)
2222
2247
  }
2223
2248
  session.persistConflictCount += 1
2224
2249
  logger.warn(`Session ${sessionId} persist conflict: storage changed agent-owned fields; skipping overwrite`, { sessionId })
2250
+ markPersistDegraded(session, attempt)
2225
2251
  return null
2226
2252
  }
2227
2253
  }
2228
2254
  session.persistConflictCount += 1
2229
2255
  logger.warn(`Session ${sessionId} persist still conflicting after ${maxAttempts} merge retries`, { sessionId, reason: lastConflict?.message })
2256
+ markPersistDegraded(session, maxAttempts)
2230
2257
  return null
2231
2258
  }
2232
2259
 
@@ -2239,14 +2266,7 @@ async function persistSessionUnlocked(session) {
2239
2266
 
2240
2267
  if (messages.length === 0) {
2241
2268
  try {
2242
- if (isSessionStateAuthoritative()) deleteSessionState(sessionId, { expectedRevision: session.persistedStorageRevision })
2243
- else {
2244
- await deleteSessionValue(sessionId)
2245
- await atomicSessionMetadataUpdate(scope, projectId, (data) => {
2246
- delete data[sessionId]
2247
- return data
2248
- })
2249
- }
2269
+ deleteSessionState(sessionId, { expectedRevision: session.persistedStorageRevision })
2250
2270
  session.persistedStorageRevision = null
2251
2271
  session.persistedStateVersion = null
2252
2272
  session.persistedStateJson = null
@@ -2353,34 +2373,14 @@ async function persistSessionUnlocked(session) {
2353
2373
  idleRetention: session.idleRetention || undefined,
2354
2374
  }
2355
2375
 
2356
- // Write body + metadata as one authoritative SQLite transaction after cutover.
2376
+ // Write body + metadata as one authoritative SQLite transaction.
2357
2377
  let persistedMetadata
2358
2378
  try {
2359
- if (isSessionStateAuthoritative()) {
2360
- const saved = await persistAuthoritativeSessionState(session, sessionData, metadata)
2361
- if (!saved) return null
2362
- session.persistedStorageRevision = saved.revision
2363
- session.persistedStateVersion = saved.stateVersion
2364
- persistedMetadata = saved.metadata
2365
- } else {
2366
- await writeSessionValue(sessionId, sessionData)
2367
- await atomicSessionMetadataUpdate(scope, projectId, (data) => {
2368
- const existingMetadata = data[sessionId]
2369
- const archivedAt = existingMetadata?.archivedAt
2370
- // Merge onto the existing record so storage-only fields the in-memory
2371
- // session does not own (pinnedAt, archivedAt, …) survive the rebuild.
2372
- persistedMetadata = {
2373
- ...existingMetadata,
2374
- ...metadata,
2375
- ...(archivedAt ? { archivedAt } : {}),
2376
- }
2377
- data[sessionId] = persistedMetadata
2378
- return data
2379
- })
2380
- if (persistedMetadata?.archivedAt) {
2381
- await writeSessionValue(sessionId, { ...sessionData, archivedAt: persistedMetadata.archivedAt })
2382
- }
2383
- }
2379
+ const saved = await persistAuthoritativeSessionState(session, sessionData, metadata)
2380
+ if (!saved) return null
2381
+ session.persistedStorageRevision = saved.revision
2382
+ session.persistedStateVersion = saved.stateVersion
2383
+ persistedMetadata = saved.metadata
2384
2384
  } catch (err) {
2385
2385
  logger.error(`Failed to persist session ${sessionId}:`, err, { sessionId })
2386
2386
  return null
@@ -2410,11 +2410,10 @@ const SLOW_PERSIST_LOG_MS = 1_000
2410
2410
 
2411
2411
  async function persistSession(session) {
2412
2412
  const startedAt = performance.now()
2413
- // Serialize per session once SQLite state is authoritative (per-row revision
2414
- // CAS guarantees correctness); cross-session persists then run independently
2415
- // instead of piling onto one global chain that also gates destroyAgent.
2416
- // JSON-mirror mode keeps the global lock for bucket-level atomicity.
2417
- const lockKey = isSessionStateAuthoritative() ? `session:${session.sessionId}` : ''
2413
+ // Serialize per session (per-row revision CAS guarantees correctness);
2414
+ // cross-session persists run independently instead of piling onto one
2415
+ // global chain that also gates destroyAgent.
2416
+ const lockKey = `session:${session.sessionId}`
2418
2417
  const result = await withSessionPersistenceLock(() => persistSessionUnlocked(session), lockKey)
2419
2418
  const durationMs = performance.now() - startedAt
2420
2419
  if (durationMs >= SLOW_PERSIST_LOG_MS) {
@@ -2976,6 +2975,7 @@ export function getSessionState(sessionId) {
2976
2975
  acpSession: session.harness === AGENT_HARNESS_OPENCODE ? session.agent.state.acpSession : undefined,
2977
2976
  isStreaming: session.abortPending ? false : session.agent.state.isStreaming,
2978
2977
  errorMessage: session.agent.state.errorMessage,
2978
+ persistDegraded: session.persistDegraded ? true : undefined,
2979
2979
  }
2980
2980
  }
2981
2981
 
@@ -3008,6 +3008,7 @@ export function getSessionStatus(sessionId) {
3008
3008
  errorMessage: session.agent.state.errorMessage,
3009
3009
  messageCount: messages.length,
3010
3010
  lastMessageTimestamp: lastMessage?.timestamp ?? null,
3011
+ persistDegraded: session.persistDegraded ? true : undefined,
3011
3012
  }
3012
3013
  }
3013
3014
 
@@ -3039,6 +3040,21 @@ export function releaseSse(sessionId) {
3039
3040
  if (session) session.sseConnected = false
3040
3041
  }
3041
3042
 
3043
+ /**
3044
+ * Count sessions with a live SSE connection — the background-migration idle
3045
+ * signal's "no active stream" input
3046
+ * (docs/architecture/session-storage-background-migration-design.zh-CN.md
3047
+ * §3.3). Derived from the per-session sseConnected flags the SSE route
3048
+ * already maintains, so no extra bookkeeping is required.
3049
+ */
3050
+ export function countActiveSseStreams() {
3051
+ let count = 0
3052
+ for (const session of agentSessions.values()) {
3053
+ if (session.sseConnected) count += 1
3054
+ }
3055
+ return count
3056
+ }
3057
+
3042
3058
  /**
3043
3059
  * Get the event bus for a session (for SSE connections).
3044
3060
  */
@@ -3128,8 +3144,8 @@ async function restoreAgentUnlocked(sessionId) {
3128
3144
  // Read the authoritative storage record once: the body (split marker),
3129
3145
  // revision for CAS, and the stored message rows (count + tail digest) used
3130
3146
  // for split-representation conflict detection after restore.
3131
- const record = isSessionStateAuthoritative() ? readSessionStateRecord(sessionId) : null
3132
- const storedMessages = isSessionStateAuthoritative() && record?.state?.messageStorage === 'split'
3147
+ const record = readSessionStateRecord(sessionId)
3148
+ const storedMessages = record?.state?.messageStorage === 'split'
3133
3149
  ? storedMessagesState(sessionId)
3134
3150
  : null
3135
3151
 
@@ -3156,9 +3172,9 @@ async function restoreAgentUnlocked(sessionId) {
3156
3172
  openCodeUsage: sessionData.openCodeUsage || null,
3157
3173
  idleRetention: sessionData.idleRetention || null,
3158
3174
  stateVersion: sessionData.stateVersion,
3159
- persistedStateVersion: isSessionStateAuthoritative() ? sessionData.stateVersion : null,
3160
- persistedStorageRevision: isSessionStateAuthoritative() ? record?.revision ?? null : null,
3161
- persistedStateJson: isSessionStateAuthoritative() ? canonicalStateJson(stripStorageOwnedStateFields(record?.state || {})) : null,
3175
+ persistedStateVersion: sessionData.stateVersion,
3176
+ persistedStorageRevision: record?.revision ?? null,
3177
+ persistedStateJson: canonicalStateJson(stripStorageOwnedStateFields(record?.state || {})),
3162
3178
  persistedMessageStorage: record?.state?.messageStorage === 'split' ? 'split' : null,
3163
3179
  persistedMessageCount: storedMessages?.count ?? (Array.isArray(sessionData.messages) ? sessionData.messages.length : 0),
3164
3180
  persistedTailDigest: storedMessages?.tailDigest || sessionMessagesTailDigest(sessionData.messages),
@@ -7,10 +7,20 @@ const defaultStorage = { atomicSessionValueUpdate, atomicUpdate, atomicSessionRe
7
7
  export const AUTO_ARCHIVE_SETTINGS_KEY = 'auto-archive-settings'
8
8
  export const AUTO_ARCHIVE_AFTER_MS = 30 * 24 * 60 * 60 * 1000
9
9
  const AUTO_ARCHIVE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000
10
+ export const AUTO_ARCHIVE_INITIAL_DELAY_MS = 30_000
10
11
 
11
12
  let autoArchiveTimer = null
13
+ let autoArchiveInitialTimer = null
12
14
  let autoArchiveRunning = false
13
15
 
16
+ // Yield one macrotask turn between heavy archive commits: each archive is a
17
+ // synchronous SQLite transaction (or JSON read-modify-write) that blocks the
18
+ // thread while it runs, and without yields a large batch of back-to-back
19
+ // archives starves every pending HTTP request for the whole batch.
20
+ function yieldToEventLoop() {
21
+ return new Promise((resolve) => setImmediate(resolve))
22
+ }
23
+
14
24
  export function normalizeAutoArchiveSettings(value) {
15
25
  if (!value || typeof value !== 'object') return { enabled: false }
16
26
  return { enabled: value.enabled === true }
@@ -65,8 +75,25 @@ async function archiveInactiveSessionsUnlocked(options = {}) {
65
75
  const metadataStore = await storage.readStore('sessions-metadata')
66
76
  const candidates = []
67
77
 
78
+ // Metadata-first scan: metadata alone decides activeness whenever it carries
79
+ // any timestamp, so the common case never loads the session body (loading and
80
+ // JSON.parsing every 2.9GB-library body is what stalled the event loop for
81
+ // ~minutes on startup). The body is only read when metadata has no usable
82
+ // timestamp at all. The scan is therefore deliberately conservative — a body
83
+ // newer than its (stale) metadata is treated as a candidate — which is safe:
84
+ // a wrongly suspected candidate is rejected by the full-state re-check inside
85
+ // the archive transactions below, and a missed candidate is simply picked up
86
+ // by the next scan. persistSession refreshes metadata timestamps on every
87
+ // save, so active sessions always stay fresh in metadata.
68
88
  for (const [sessionId, metadata] of Object.entries(metadataStore)) {
69
89
  if (!metadata || metadata.archivedAt || metadata.messageCount === 0 || metadata.taskStatus === 'running') continue
90
+ const metadataActivityTime = sessionActivityTime(metadata)
91
+ if (metadataActivityTime !== null) {
92
+ if (metadataActivityTime >= cutoff) continue
93
+ candidates.push(sessionId)
94
+ continue
95
+ }
96
+ // Metadata carries no timestamp at all: fall back to the body.
70
97
  const session = await storage.readSessionValue(sessionId)
71
98
  if (!session || session.archivedAt) continue
72
99
  const activityTime = sessionActivityTime(metadata, session)
@@ -75,10 +102,13 @@ async function archiveInactiveSessionsUnlocked(options = {}) {
75
102
  }
76
103
 
77
104
  const archivedSessionIds = []
78
- if (typeof storage.atomicSessionRecordUpdate === 'function' && await storage.isSessionStateAuthoritative()) {
79
- // Authoritative: archive body + metadata in one SQLite transaction (CAS
80
- // retried on concurrent metadata-only changes), so no half-archived state.
81
- for (const sessionId of candidates) {
105
+ // Authoritative: archive body + metadata in one SQLite transaction (CAS
106
+ // retried on concurrent metadata-only changes), so no half-archived state.
107
+ if (typeof storage.atomicSessionRecordUpdate === 'function') {
108
+ for (const [index, sessionId] of candidates.entries()) {
109
+ // Let pending requests run between archive transactions (not before the
110
+ // first candidate — nothing to yield from yet).
111
+ if (index > 0) await yieldToEventLoop()
82
112
  const updated = await storage.atomicSessionRecordUpdate(sessionId, ({ state, metadata }) => {
83
113
  const activityTime = sessionActivityTime(metadata, state)
84
114
  if (state.archivedAt || metadata.archivedAt || metadata.messageCount === 0 || metadata.taskStatus === 'running' || activityTime === null || activityTime >= cutoff) return null
@@ -87,7 +117,10 @@ async function archiveInactiveSessionsUnlocked(options = {}) {
87
117
  if (updated?.state?.archivedAt === archivedAt) archivedSessionIds.push(sessionId)
88
118
  }
89
119
  } else {
90
- for (const sessionId of candidates) {
120
+ for (const [index, sessionId] of candidates.entries()) {
121
+ // Let pending requests run between archive transactions (not before the
122
+ // first candidate — nothing to yield from yet).
123
+ if (index > 0) await yieldToEventLoop()
91
124
  const updatedSession = await storage.atomicSessionValueUpdate(sessionId, (session) => {
92
125
  const activityTime = sessionActivityTime(session)
93
126
  if (session.archivedAt || activityTime === null || activityTime >= cutoff) return session
@@ -134,16 +167,30 @@ export async function archiveInactiveSessions(options = {}) {
134
167
  }
135
168
  }
136
169
 
137
- export function startAutoArchiveRunner() {
170
+ // `options` exists for tests (inject storage/now like archiveInactiveSessions);
171
+ // production callers pass nothing and get defaultStorage + Date.now().
172
+ export function startAutoArchiveRunner(options = {}) {
138
173
  if (autoArchiveTimer) return
139
174
  autoArchiveTimer = setInterval(() => {
140
- archiveInactiveSessions().catch((error) => logger.error('Automatic conversation archive failed:', error))
175
+ archiveInactiveSessions(options).catch((error) => logger.error('Automatic conversation archive failed:', error))
141
176
  }, AUTO_ARCHIVE_CHECK_INTERVAL_MS)
142
177
  autoArchiveTimer.unref?.()
143
- archiveInactiveSessions().catch((error) => logger.error('Initial automatic conversation archive failed:', error))
178
+ // Delay the first pass off the startup path: right after listen() the server
179
+ // is absorbing the browser's initial request burst, and the scan + archive
180
+ // transactions on a large library can block the event loop for a long
181
+ // stretch. The timer is unref'd so it never keeps the process alive.
182
+ autoArchiveInitialTimer = setTimeout(() => {
183
+ autoArchiveInitialTimer = null
184
+ archiveInactiveSessions(options).catch((error) => logger.error('Initial automatic conversation archive failed:', error))
185
+ }, AUTO_ARCHIVE_INITIAL_DELAY_MS)
186
+ autoArchiveInitialTimer.unref?.()
144
187
  }
145
188
 
146
189
  export function stopAutoArchiveRunner() {
190
+ if (autoArchiveInitialTimer) {
191
+ clearTimeout(autoArchiveInitialTimer)
192
+ autoArchiveInitialTimer = null
193
+ }
147
194
  if (!autoArchiveTimer) return
148
195
  clearInterval(autoArchiveTimer)
149
196
  autoArchiveTimer = null