@shawnstack/quickforge 1.7.8 → 1.7.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/quickforge.mjs +10 -1
- package/dist/assets/{AgentProfilesPage-BSM5w3bn.js → AgentProfilesPage-Dw9orPNt.js} +1 -1
- package/dist/assets/{ChatPanelHost-cd80S4EX.js → ChatPanelHost-CW3-MQyL.js} +2 -2
- package/dist/assets/{CloudAccountSettingsPage-BJMrfgy9.js → CloudAccountSettingsPage-CDkKN1Sz.js} +1 -1
- package/dist/assets/{PluginsPage-mJkTmVVV.js → PluginsPage-Dj1bmsEf.js} +1 -1
- package/dist/assets/{ScheduledTasksPage-DJj_NLm9.js → ScheduledTasksPage-C9lOTi75.js} +1 -1
- package/dist/assets/{SettingsWorkspacePage-7lQ0Wl63.js → SettingsWorkspacePage-CFuvSATi.js} +3 -3
- package/dist/assets/{ShareLinksSettingsPage-DjFq_Lnb.js → ShareLinksSettingsPage-CH_t33A3.js} +1 -1
- package/dist/assets/{SharedConversationPage-BbuNpcpP.js → SharedConversationPage-DLo05j7c.js} +1 -1
- package/dist/assets/{TerminalDock-CczcCJJE.js → TerminalDock-CSOVub-m.js} +1 -1
- package/dist/assets/{WorkspaceInspector-0i6x-diB.js → WorkspaceInspector-DPXgLIum.js} +1 -1
- package/dist/assets/index-DQBOm9gt.js +66 -0
- package/dist/assets/index-ps85guTs.css +3 -0
- package/dist/assets/{local-tools-Dn1Y9aPe.js → local-tools-BxfQeIUQ.js} +1 -1
- package/dist/assets/{mcp-servers-dialog-BG4b67ea.js → mcp-servers-dialog-CwHp0K4q.js} +1 -1
- package/dist/assets/{skills-dialog-DIzZvQqW.js → skills-dialog-k9ycekIU.js} +1 -1
- package/dist/index.html +4 -4
- package/package.json +1 -1
- package/server/agent-manager.mjs +68 -2
- package/server/auto-compaction.mjs +0 -2
- package/server/context-usage.mjs +9 -7
- package/server/maintenance/downgrade-session-state-v1.mjs +4 -2
- package/server/maintenance/export-session-state-v1.mjs +4 -2
- package/server/public-api.mjs +21 -2
- package/server/routes/backup.mjs +7 -0
- package/server/routes/storage.mjs +23 -0
- package/server/session-persistence-lock.mjs +17 -6
- package/server/session-state-backup.mjs +4 -2
- package/server/session-state-cutover.mjs +284 -91
- package/server/session-state-service.mjs +38 -17
- package/server/sqlite/session-state-repository.mjs +145 -25
- package/server/storage.mjs +28 -0
- package/server/utils/process-tree.mjs +14 -2
- package/dist/assets/index-D-J_8Smf.css +0 -3
- package/dist/assets/index-DFASc15l.js +0 -66
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/mermaid.core-DWpShXCs.js","assets/pi-ai-Cr2ee4a0.js","assets/rolldown-runtime-DWdDZTNf.js","assets/chunk-HOUHSVGY-tdc-Z9eW.js","assets/src-PYsDme0X.js","assets/chunk-Y2CYZVJY-D6N-CrJv.js","assets/chunk-WYO6CB5R-NF-MsJJR.js","assets/chunk-ICXQ74PX-Cd7igVGt.js","assets/dist-CLg13NtV.js","assets/chunk-52WLFC77-CDbaHw80.js","assets/line-B4DxX7tC.js","assets/path-6uRLdFF7.js","assets/array-B9UHiPd-.js","assets/chunk-7BUUIJ7U-CCcsI9XE.js","assets/chunk-C7G6YPKG-ZMLUvfLy.js","assets/chunk-OGEWGWER-TgvkXkFX.js","assets/chunk-Q4XR5HBZ-fo5-HsqY.js","assets/pi-web-ui-DV5jKjSg.js","assets/lit-vendor-DvaQX_UQ.js","assets/chunk-ZGVPDNZ5-CRrAYi4g.js","assets/rough.esm-Bud4g4fh.js","assets/chunk-FWX5IMBZ-DrnrdrSu.js","assets/chunk-VAUOI2AC-CWYjg8zE.js","assets/chunk-ZIRB5QZD-Dc1s5bLM.js"])))=>i.map(i=>d[i]);
|
|
2
|
-
import{d as e,p as t,t as n,u as r}from"./lit-vendor-DvaQX_UQ.js";import{y as i}from"./pi-web-ui-DV5jKjSg.js";import{J as a}from"./pi-ai-Cr2ee4a0.js";import{$t as o,At as s,Dt as c,Et as l,Gt as u,Jt as d,Kt as f,Ot as p,Pn as m,Qt as h,Xt as g,Yt as _,Zt as v,d as y,en as b,in as x,nn as S,ot as C,qt as w,r as T,rn as ee,tn as E}from"./index-
|
|
2
|
+
import{d as e,p as t,t as n,u as r}from"./lit-vendor-DvaQX_UQ.js";import{y as i}from"./pi-web-ui-DV5jKjSg.js";import{J as a}from"./pi-ai-Cr2ee4a0.js";import{$t as o,At as s,Dt as c,Et as l,Gt as u,Jt as d,Kt as f,Ot as p,Pn as m,Qt as h,Xt as g,Yt as _,Zt as v,d as y,en as b,in as x,nn as S,ot as C,qt as w,r as T,rn as ee,tn as E}from"./index-DQBOm9gt.js";function D(e){return!!(e&&typeof e==`object`&&!Array.isArray(e))}function O(e,t,n){if(!e||e.isError||t||!D(e.details))return``;let r=e.details.action,i=e.details.status,a=e.details.markdown;if(typeof a!=`string`)return``;if(r===`read`){if(a)return a;if(i===`empty`||i===void 0)return n(`memoryContentEmpty`)}return r===`write`&&(i===`saved`||i===void 0)?n(`memoryContentSaved`):``}var te=/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.(?:png|jpg|webp|gif)$/i,ne=new Set([`image/png`,`image/jpeg`,`image/webp`,`image/gif`]);function re(e){return!!(e&&typeof e==`object`&&!Array.isArray(e))}function k(e){if(!re(e)||e.type!==`generated_image_result`||typeof e.sessionId!=`string`||!e.sessionId||typeof e.prompt!=`string`||typeof e.model!=`string`||!Array.isArray(e.assets))return null;let t=e.assets.flatMap(e=>!re(e)||typeof e.assetId!=`string`||!te.test(e.assetId)||typeof e.mimeType!=`string`||!ne.has(e.mimeType)||typeof e.size!=`number`||!Number.isFinite(e.size)||e.size<0?[]:[{assetId:e.assetId,mimeType:e.mimeType,size:e.size}]);return t.length?{type:`generated_image_result`,sessionId:e.sessionId,prompt:e.prompt,model:e.model,assets:t,...typeof e.text==`string`&&e.text?{text:e.text}:{}}:null}function ie(e,t,n=window.location.pathname){let r=n.match(/^\/share\/([^/]+)\/?$/)?.[1];return r?`/api/shared/${encodeURIComponent(decodeURIComponent(r))}/assets/${encodeURIComponent(t.assetId)}`:`/api/session-assets/${encodeURIComponent(e.sessionId)}/${encodeURIComponent(t.assetId)}`}var A=`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>`,j=`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>`,M=`<svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/></svg>`,N=`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><circle cx="18" cy="6" r="3"/><path d="M18 9v1a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2V9"/><path d="M12 12v3"/></svg>`,P=`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 2v6h-6"/><path d="M3 12a9 9 0 0 1 15.36-5.64L21 9"/><path d="M3 22v-6h6"/><path d="M21 12a9 9 0 0 1-15.36 5.64L3 15"/></svg>`,ae=`<svg class="quickforge-plan-mode-icon" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6h1"/><path d="M4 12h1"/><path d="M4 18h1"/><path d="M9 6h11"/><path d="M9 12h11"/><path d="M9 18h11"/></svg>`,oe=`<svg class="quickforge-plan-remove-icon" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>`,se=`<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14"/><path d="M5 12h14"/></svg>`,ce=`<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>`,F=`<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7.5 4.5h5a1.5 1.5 0 0 1 1.5 1.5v2h1a2.5 2.5 0 0 1 0 5h-1v2a1.5 1.5 0 0 1-1.5 1.5h-2v1a2.5 2.5 0 0 1-5 0v-1h-2A1.5 1.5 0 0 1 2 15.5v-3h1a2 2 0 1 0 0-4H2v-2A1.5 1.5 0 0 1 3.5 5h2v-1a2.5 2.5 0 0 1 5 0v.5Z"/></svg>`,le=`<svg viewBox="0 0 20 20" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.65" stroke-linecap="round" stroke-linejoin="round"><path d="M5.4 2.8h6.1L15.8 7v10.2H5.4z"/><path d="M11.4 2.9V7h4.1"/><path d="M7.6 10.2h5"/><path d="M7.6 13h4.3"/></svg>`,ue=`<svg viewBox="0 0 20 20" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.65" stroke-linecap="round" stroke-linejoin="round"><rect x="3.4" y="4" width="13.2" height="12.2" rx="1.5"/><path d="M3.4 8h13.2"/><path d="M7.8 4v12.2"/><path d="M12.2 4v12.2"/><path d="M3.4 12h13.2"/></svg>`,de=`<svg viewBox="0 0 20 20" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.65" stroke-linecap="round" stroke-linejoin="round"><path d="M3 4.2h14"/><rect x="4.2" y="4.2" width="11.6" height="8.4" rx="1.2"/><path d="M10 12.6v3.2"/><path d="m7.2 17 2.8-1.2 2.8 1.2"/><path d="M7.1 9.5 9 7.7l1.5 1.3 2.4-2.5"/></svg>`,fe=`<svg class="quickforge-agent-access-trigger-icon" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3 19 6v5c0 4.5-2.8 8.4-7 10-4.2-1.6-7-5.5-7-10V6l7-3Z"/><path d="M9 12h6"/></svg>`,pe=`<svg class="quickforge-agent-access-option-icon" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m21.73 18-8-14a2 2 0 0 0-3.46 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>`,me=`<svg class="quickforge-agent-access-check" xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>`,he=`<svg class="quickforge-agent-access-chevron" xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>`,ge=`<svg class="quickforge-opencode-config-icon" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20V10"/><path d="M18 20V4"/><path d="M6 20v-4"/><path d="M18 4a2 2 0 0 0-4 0"/><path d="M6 16a2 2 0 0 0-4 0"/><path d="M12 10a2 2 0 0 0-4 0"/></svg>`;function _e(e){return m(e===`full-access`?`agentAccessFullLabel`:`agentAccessDefaultLabel`)}function I(e){let t=document.querySelector(`.quickforge-agent-access-menu`);t?.__quickforgeDismissHandler&&(document.removeEventListener(`pointerdown`,t.__quickforgeDismissHandler,!0),document.removeEventListener(`keydown`,t.__quickforgeDismissHandler,!0),window.removeEventListener(`resize`,t.__quickforgeDismissHandler,!0),window.removeEventListener(`scroll`,t.__quickforgeDismissHandler,!0),t.__quickforgeDismissHandler=void 0),t?.remove(),e.querySelector(`.quickforge-agent-access-inline`)?.setAttribute(`aria-expanded`,`false`)}function ve(e,t,n){let r=e===t,i=document.createElement(`button`);return i.type=`button`,i.className=`quickforge-agent-access-item`,i.setAttribute(`role`,`menuitemradio`),i.setAttribute(`aria-checked`,String(r)),i.dataset.quickforgeAgentAccessMode=e,i.innerHTML=`
|
|
3
3
|
<span class="quickforge-agent-access-check-slot">${r?me:``}</span>
|
|
4
4
|
<span class="quickforge-agent-access-option-icon-wrap">${e===`full-access`?pe:fe}</span>
|
|
5
5
|
<span class="quickforge-agent-access-item-label"></span>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{i as e}from"./rolldown-runtime-DWdDZTNf.js";import{D as t,Gt as n,J as r,Jt as i,S as a,ft as o,j as s,s as c,v as l}from"./icons-DM_qJ7VJ.js";import{n as u}from"./react-vendor-BpWW_Nvp.js";import{An as d,Dn as f,H as p,Pn as m,Tt as h,kn as g}from"./index-
|
|
1
|
+
import{i as e}from"./rolldown-runtime-DWdDZTNf.js";import{D as t,Gt as n,J as r,Jt as i,S as a,ft as o,j as s,s as c,v as l}from"./icons-DM_qJ7VJ.js";import{n as u}from"./react-vendor-BpWW_Nvp.js";import{An as d,Dn as f,H as p,Pn as m,Tt as h,kn as g}from"./index-DQBOm9gt.js";var _=e(i(),1),v=u();function y(e){return e===`connected`?`bg-emerald-500/12 text-emerald-700 dark:text-emerald-300`:e===`error`?`bg-destructive/12 text-destructive`:e===`disabled`?`bg-muted text-muted-foreground`:`bg-amber-500/12 text-amber-700 dark:text-amber-300`}var b=12;function x({server:e,toggling:t,reconnecting:n,onToggle:i,onEdit:o,onDelete:l,onReconnect:u}){let f=e.tools?.slice(0,b)??[],p=e.toolCount??e.tools?.length??0,h=Math.max(0,p-f.length),_=e.enabled&&e.status!==`connected`;return(0,v.jsxs)(`div`,{className:`rounded-lg border border-border p-3`,children:[(0,v.jsxs)(`div`,{className:`flex items-start justify-between gap-3`,children:[(0,v.jsxs)(`div`,{className:`min-w-0`,children:[(0,v.jsxs)(`div`,{className:`flex flex-wrap items-center gap-2`,children:[(0,v.jsx)(`div`,{className:`truncate text-sm font-medium text-foreground/90`,children:e.name}),(0,v.jsx)(`span`,{className:d(`rounded-full px-2 py-0.5 text-[11px]`,y(e.status)),children:e.status||`unknown`}),(0,v.jsx)(`span`,{className:`text-[11px] text-muted-foreground/60`,children:m(`mcpToolsCount`,{count:p})})]}),(0,v.jsx)(`div`,{className:`mt-1 truncate text-xs text-muted-foreground/65`,children:e.transport===`stdio`?`${e.command} ${(e.args||[]).join(` `)}`:e.url}),e.error?(0,v.jsx)(`div`,{className:`mt-1 text-xs text-destructive`,children:e.error}):null]}),(0,v.jsxs)(`div`,{className:`flex shrink-0 items-center gap-1`,children:[(0,v.jsx)(`button`,{type:`button`,role:`switch`,"aria-checked":e.enabled,disabled:t,className:d(`relative h-6 w-11 rounded-full transition-colors disabled:cursor-not-allowed disabled:opacity-60`,e.enabled?`bg-emerald-500`:`bg-muted-foreground/30`),onClick:()=>i(e),title:e.enabled?m(`pauseTask`):m(`enable`),children:(0,v.jsx)(`span`,{className:d(`absolute left-0.5 top-0.5 size-5 rounded-full bg-white shadow transition-transform`,e.enabled?`translate-x-5`:`translate-x-0`)})}),_?(0,v.jsx)(g,{type:`button`,variant:`ghost`,size:`icon`,className:`size-8 text-muted-foreground`,onClick:()=>u(e.name),disabled:n,"aria-label":m(`mcpReconnectServer`),title:m(`mcpReconnectServer`),children:n?(0,v.jsx)(r,{className:`size-4 animate-spin`}):(0,v.jsx)(a,{className:`size-4`})}):null,(0,v.jsx)(g,{type:`button`,variant:`ghost`,size:`icon`,className:`size-8 text-muted-foreground`,onClick:()=>o(e),"aria-label":m(`editTask`),title:m(`editTask`),children:(0,v.jsx)(s,{className:`size-4`})}),(0,v.jsx)(g,{type:`button`,variant:`ghost`,size:`icon`,className:`size-8 text-destructive`,onClick:()=>l(e.name),"aria-label":m(`delete`),title:m(`delete`),children:(0,v.jsx)(c,{className:`size-4`})})]})]}),f.length>0?(0,v.jsxs)(`div`,{className:`mt-2 flex flex-wrap gap-1.5`,children:[f.map(e=>(0,v.jsx)(`span`,{className:`rounded-md bg-muted/28 px-1.5 py-0.5 text-[11px] text-muted-foreground/75`,title:e.quickForgeName,children:e.name},e.quickForgeName)),h>0?(0,v.jsx)(`span`,{className:`rounded-md px-1.5 py-0.5 text-[11px] text-muted-foreground/55`,children:m(`mcpMoreTools`,{count:h})}):null]}):null]})}function S(){return{name:``,transport:`stdio`,command:``,args:[],url:``,cwd:``,env:{}}}function C(e){return{name:e.name,transport:e.transport,command:e.command,args:e.args||[],url:e.url||``,cwd:e.cwd||``,env:e.env||{}}}function w(e){return(e||[]).join(`
|
|
2
2
|
`)}function T(e){return Object.entries(e||{}).map(([e,t])=>`${e}=${t}`).join(`
|
|
3
3
|
`)}function E(e){return e.split(`
|
|
4
4
|
`).map(e=>e.trim()).filter(Boolean)}function D(e){let t={};for(let n of e.split(`
|
|
@@ -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-
|
|
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(`
|
|
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-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-DQBOm9gt.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-
|
|
26
|
-
<link rel="modulepreload" crossorigin href="/assets/skills-dialog-
|
|
27
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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">
|
|
28
28
|
</head>
|
|
29
29
|
<body>
|
|
30
30
|
<div id="root"></div>
|
package/package.json
CHANGED
package/server/agent-manager.mjs
CHANGED
|
@@ -2403,8 +2403,28 @@ async function persistSessionUnlocked(session) {
|
|
|
2403
2403
|
return persistedMetadata || metadata
|
|
2404
2404
|
}
|
|
2405
2405
|
|
|
2406
|
+
// Persist slower than this (queue wait + synchronous SQLite write) is worth a
|
|
2407
|
+
// warning: large sessions write big synchronous transactions that stall the
|
|
2408
|
+
// event loop and make every in-flight request (including /restore) wait.
|
|
2409
|
+
const SLOW_PERSIST_LOG_MS = 1_000
|
|
2410
|
+
|
|
2406
2411
|
async function persistSession(session) {
|
|
2407
|
-
|
|
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}` : ''
|
|
2418
|
+
const result = await withSessionPersistenceLock(() => persistSessionUnlocked(session), lockKey)
|
|
2419
|
+
const durationMs = performance.now() - startedAt
|
|
2420
|
+
if (durationMs >= SLOW_PERSIST_LOG_MS) {
|
|
2421
|
+
logger.warn(`Session ${session.sessionId} persist took ${Math.round(durationMs)}ms (queue wait + write)`, {
|
|
2422
|
+
sessionId: session.sessionId,
|
|
2423
|
+
durationMs: Math.round(durationMs),
|
|
2424
|
+
messageCount: session.agent?.state?.messages?.length ?? 0,
|
|
2425
|
+
})
|
|
2426
|
+
}
|
|
2427
|
+
return result
|
|
2408
2428
|
}
|
|
2409
2429
|
|
|
2410
2430
|
export async function persistSessionState(session) {
|
|
@@ -3069,14 +3089,33 @@ export async function destroyAgent(sessionId) {
|
|
|
3069
3089
|
agentSessions.delete(sessionId)
|
|
3070
3090
|
}
|
|
3071
3091
|
|
|
3092
|
+
// In-flight restores keyed by sessionId. Concurrent route handlers
|
|
3093
|
+
// (POST /restore, GET /state, GET /messages, GET /status, SSE) all fall back
|
|
3094
|
+
// to restoreAgent; without dedupe each raced through createAgent and the last
|
|
3095
|
+
// agentSessions.set overwrote the others, leaking the overwritten sessions
|
|
3096
|
+
// (listeners, idle/persist timers, OpenCode child processes) forever.
|
|
3097
|
+
const pendingRestores = new Map()
|
|
3098
|
+
|
|
3072
3099
|
/**
|
|
3073
3100
|
* Try to restore an agent session from persisted storage.
|
|
3101
|
+
* Concurrent calls for the same session share one in-flight restore.
|
|
3074
3102
|
* Returns the restored session, or null if not found.
|
|
3075
3103
|
*/
|
|
3076
|
-
export
|
|
3104
|
+
export function restoreAgent(sessionId) {
|
|
3077
3105
|
const existing = agentSessions.get(sessionId)
|
|
3078
3106
|
if (existing) return existing
|
|
3079
3107
|
|
|
3108
|
+
const inFlight = pendingRestores.get(sessionId)
|
|
3109
|
+
if (inFlight) return inFlight
|
|
3110
|
+
|
|
3111
|
+
const restorePromise = restoreAgentUnlocked(sessionId).finally(() => {
|
|
3112
|
+
pendingRestores.delete(sessionId)
|
|
3113
|
+
})
|
|
3114
|
+
pendingRestores.set(sessionId, restorePromise)
|
|
3115
|
+
return restorePromise
|
|
3116
|
+
}
|
|
3117
|
+
|
|
3118
|
+
async function restoreAgentUnlocked(sessionId) {
|
|
3080
3119
|
try {
|
|
3081
3120
|
const sessionData = await readSessionValue(sessionId)
|
|
3082
3121
|
if (!sessionData) {
|
|
@@ -3223,6 +3262,33 @@ export async function refreshAllSessionTools() {
|
|
|
3223
3262
|
return result
|
|
3224
3263
|
}
|
|
3225
3264
|
|
|
3265
|
+
/**
|
|
3266
|
+
* Refresh the model binding of every active QuickForge session after model
|
|
3267
|
+
* configuration changes (custom providers, maxTokens, ...). OpenCode owns its
|
|
3268
|
+
* model selection and streaming sessions are skipped (runPrompt re-resolves
|
|
3269
|
+
* the binding on the next message). Only sessions whose model actually
|
|
3270
|
+
* changed get a state event.
|
|
3271
|
+
*/
|
|
3272
|
+
export async function refreshAllSessionModels() {
|
|
3273
|
+
for (const [sessionId, session] of agentSessions) {
|
|
3274
|
+
if (session.harness !== AGENT_HARNESS_QUICKFORGE) continue
|
|
3275
|
+
if (session.agent?.state?.isStreaming) continue
|
|
3276
|
+
try {
|
|
3277
|
+
const before = JSON.stringify(session.model ?? null)
|
|
3278
|
+
await refreshSessionModelBinding(session)
|
|
3279
|
+
const after = JSON.stringify(session.model ?? null)
|
|
3280
|
+
if (before === after) continue
|
|
3281
|
+
const state = getSessionState(sessionId)
|
|
3282
|
+
emitSessionEvent(session, { type: 'state', ...state })
|
|
3283
|
+
} catch (error) {
|
|
3284
|
+
// The model may have been deleted (model_not_configured); runPrompt will
|
|
3285
|
+
// surface the same error on the next message. Log and keep other
|
|
3286
|
+
// sessions refreshing.
|
|
3287
|
+
logger.error(`Failed to refresh model for session ${sessionId}:`, error, { sessionId })
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
|
|
3226
3292
|
export async function updateSessionTitle(sessionId, title) {
|
|
3227
3293
|
let session = agentSessions.get(sessionId)
|
|
3228
3294
|
if (!session) session = await restoreAgent(sessionId)
|
|
@@ -295,7 +295,6 @@ export function estimateSessionContextUsage(session, messages = session?.agent?.
|
|
|
295
295
|
estimatedInputTokens: 0,
|
|
296
296
|
knownInputTokens: 0,
|
|
297
297
|
inputTokenSource: 'estimated',
|
|
298
|
-
reservedOutputTokens: 0,
|
|
299
298
|
totalTokens: 0,
|
|
300
299
|
contextWindow,
|
|
301
300
|
percent: 0,
|
|
@@ -306,7 +305,6 @@ export function estimateSessionContextUsage(session, messages = session?.agent?.
|
|
|
306
305
|
systemPromptTokens: 0,
|
|
307
306
|
messagesTokens: 0,
|
|
308
307
|
toolsTokens: 0,
|
|
309
|
-
reservedOutputTokens: 0,
|
|
310
308
|
},
|
|
311
309
|
}
|
|
312
310
|
}
|
package/server/context-usage.mjs
CHANGED
|
@@ -50,7 +50,6 @@ function localMessagesTokens(messages) {
|
|
|
50
50
|
|
|
51
51
|
export function estimateContextUsage({ systemPrompt, messages, tools, model, minimumProviderUsageIndex = 0 }) {
|
|
52
52
|
const contextWindow = Number(model?.contextWindow) || 0
|
|
53
|
-
const reservedOutputTokens = Math.max(0, Number(model?.maxTokens) || 4096)
|
|
54
53
|
const normalizedMessages = normalizeMessagesForTokenEstimate(messages)
|
|
55
54
|
const coreEstimate = estimateContextTokens(normalizedMessages)
|
|
56
55
|
const systemPromptTokens = textTokens(systemPrompt)
|
|
@@ -68,8 +67,11 @@ export function estimateContextUsage({ systemPrompt, messages, tools, model, min
|
|
|
68
67
|
const inputTokens = providerBasedContextTokens > 0
|
|
69
68
|
? Math.max(estimatedInputTokens, providerBasedContextTokens)
|
|
70
69
|
: estimatedInputTokens
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
// 上下文占用按纯输入口径统计:percent = inputTokens / contextWindow。
|
|
71
|
+
// 真实请求的 max_tokens 由 pi-ai `clampMaxTokensToContext` 按窗口收缩,
|
|
72
|
+
// 统计侧不再预留输出 token;totalTokens 字段仅为兼容保留,恒等于 inputTokens。
|
|
73
|
+
const totalTokens = inputTokens
|
|
74
|
+
const percent = contextWindow > 0 ? Math.round((inputTokens / contextWindow) * 1000) / 10 : 0
|
|
73
75
|
const inputTokenSource = providerBasedContextTokens > 0
|
|
74
76
|
? providerBasedContextTokens >= estimatedInputTokens ? 'provider' : 'mixed'
|
|
75
77
|
: 'estimated'
|
|
@@ -80,7 +82,6 @@ export function estimateContextUsage({ systemPrompt, messages, tools, model, min
|
|
|
80
82
|
knownInputTokens: providerBasedContextTokens,
|
|
81
83
|
providerContextTokens: providerBasedContextTokens,
|
|
82
84
|
inputTokenSource,
|
|
83
|
-
reservedOutputTokens,
|
|
84
85
|
totalTokens,
|
|
85
86
|
contextWindow,
|
|
86
87
|
percent,
|
|
@@ -88,7 +89,6 @@ export function estimateContextUsage({ systemPrompt, messages, tools, model, min
|
|
|
88
89
|
systemPromptTokens,
|
|
89
90
|
messagesTokens,
|
|
90
91
|
toolsTokens,
|
|
91
|
-
reservedOutputTokens,
|
|
92
92
|
providerUsageTokens,
|
|
93
93
|
trailingTokens: providerUsageTokens > 0
|
|
94
94
|
? Math.max(0, Number(coreEstimate.trailingTokens) || 0)
|
|
@@ -101,13 +101,15 @@ export function estimateContextUsage({ systemPrompt, messages, tools, model, min
|
|
|
101
101
|
|
|
102
102
|
export function shouldCompactContextByPercent(usage, thresholdPercent) {
|
|
103
103
|
const contextWindow = Number(usage?.contextWindow) || 0
|
|
104
|
-
|
|
104
|
+
// 阈值按纯输入占用判断:inputTokens / contextWindow ≥ thresholdPercent 即触发,
|
|
105
|
+
// 不包含预留输出 token(真实请求的 max_tokens 由 pi-ai 按窗口收缩)。
|
|
106
|
+
const inputTokens = Math.max(0, Number(usage?.inputTokens) || 0)
|
|
105
107
|
const threshold = Math.min(100, Math.max(0, Number(thresholdPercent) || 0))
|
|
106
108
|
if (!contextWindow) return false
|
|
107
109
|
|
|
108
110
|
const thresholdTokens = Math.ceil(contextWindow * threshold / 100)
|
|
109
111
|
const reserveTokens = Math.min(contextWindow, Math.max(0, contextWindow - thresholdTokens + 1))
|
|
110
|
-
return shouldCompact(
|
|
112
|
+
return shouldCompact(inputTokens, contextWindow, {
|
|
111
113
|
enabled: true,
|
|
112
114
|
reserveTokens,
|
|
113
115
|
keepRecentTokens: 0,
|
|
@@ -81,8 +81,10 @@ try {
|
|
|
81
81
|
const integrity = repository.verifyIntegrity({ quickCheck: true })
|
|
82
82
|
if (!integrity.ok) throw new Error('Session state integrity verification failed')
|
|
83
83
|
const snapshot = repository.exportSnapshot()
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
// Lightweight integrity has no digest; the count cross-check plus the
|
|
85
|
+
// snapshot's own digest stay authoritative.
|
|
86
|
+
if (snapshot.count !== integrity.count) {
|
|
87
|
+
throw new Error('Session state snapshot count verification failed')
|
|
86
88
|
}
|
|
87
89
|
const report = {
|
|
88
90
|
ok: true,
|
|
@@ -33,8 +33,10 @@ try {
|
|
|
33
33
|
const integrity = repository.verifyIntegrity({ quickCheck: true })
|
|
34
34
|
if (!integrity.ok) throw new Error('Session state integrity verification failed')
|
|
35
35
|
const snapshot = repository.exportSnapshot()
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
// Lightweight integrity has no digest; the count cross-check plus the
|
|
37
|
+
// snapshot's own digest stay authoritative.
|
|
38
|
+
if (snapshot.count !== integrity.count) {
|
|
39
|
+
throw new Error('Session state backup count verification failed')
|
|
38
40
|
}
|
|
39
41
|
const backup = {
|
|
40
42
|
app: 'quickforge',
|
package/server/public-api.mjs
CHANGED
|
@@ -13,6 +13,17 @@ function sleep(ms) {
|
|
|
13
13
|
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
const STARTUP_HEALTH_TIMEOUT_MS = 300000
|
|
17
|
+
|
|
18
|
+
function isProcessRunning(pid) {
|
|
19
|
+
try {
|
|
20
|
+
process.kill(pid, 0)
|
|
21
|
+
return true
|
|
22
|
+
} catch {
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
16
27
|
function normalizeHost(host) {
|
|
17
28
|
return host || '127.0.0.1'
|
|
18
29
|
}
|
|
@@ -135,12 +146,20 @@ export async function checkQuickForgeHealth(options = {}) {
|
|
|
135
146
|
}
|
|
136
147
|
|
|
137
148
|
async function waitForQuickForge(options = {}) {
|
|
138
|
-
const timeoutMs = options.timeoutMs ||
|
|
149
|
+
const timeoutMs = options.timeoutMs || STARTUP_HEALTH_TIMEOUT_MS
|
|
139
150
|
const deadline = Date.now() + timeoutMs
|
|
151
|
+
const expectedPid = options.expectedPid || null
|
|
140
152
|
|
|
141
153
|
while (Date.now() < deadline) {
|
|
142
154
|
const health = await checkQuickForgeHealth(options)
|
|
143
155
|
if (health) return health
|
|
156
|
+
if (expectedPid && !isProcessRunning(expectedPid)) {
|
|
157
|
+
// The spawned server exited. Give the async 'exit' event a moment to
|
|
158
|
+
// dispatch so startQuickForge can report the real exit code/signal
|
|
159
|
+
// instead of a generic timeout.
|
|
160
|
+
await sleep(300)
|
|
161
|
+
return null
|
|
162
|
+
}
|
|
144
163
|
await sleep(options.pollIntervalMs || 300)
|
|
145
164
|
}
|
|
146
165
|
|
|
@@ -213,7 +232,7 @@ export async function startQuickForge(options = {}) {
|
|
|
213
232
|
child.once('error', reject)
|
|
214
233
|
})
|
|
215
234
|
|
|
216
|
-
const health = await waitForQuickForge(options)
|
|
235
|
+
const health = await waitForQuickForge({ ...options, expectedPid: child.pid })
|
|
217
236
|
if (!health) {
|
|
218
237
|
if (!child.killed && exitInfo === null) {
|
|
219
238
|
try {
|
package/server/routes/backup.mjs
CHANGED
|
@@ -15,6 +15,8 @@ import { isLanAccessStorageAuthoritative } from '../lan-access-service.mjs'
|
|
|
15
15
|
import { isLanAccessMaintenanceActive } from '../lan-access-cutover.mjs'
|
|
16
16
|
import { exportLanAccessStateForBackup, restoreLanAccessStateSnapshot } from '../lan-access-backup.mjs'
|
|
17
17
|
import { readLanAccessJsonFile, writeLanAccessJsonFile } from '../lan-access-json-file.mjs'
|
|
18
|
+
import { refreshAllSessionModels } from '../agent-manager.mjs'
|
|
19
|
+
import { logger } from '../utils/logger.mjs'
|
|
18
20
|
import { sendJson, readJsonBody } from '../utils/response.mjs'
|
|
19
21
|
import {
|
|
20
22
|
ensureStorage,
|
|
@@ -641,6 +643,11 @@ async function restoreValidatedBackup(backup, mode = 'replace') {
|
|
|
641
643
|
if (sections.customProviders !== undefined) {
|
|
642
644
|
const value = merge ? mergeRecordStore(await readStore('custom-providers'), sections.customProviders) : sections.customProviders
|
|
643
645
|
await writeStore('custom-providers', value)
|
|
646
|
+
try {
|
|
647
|
+
await refreshAllSessionModels()
|
|
648
|
+
} catch (error) {
|
|
649
|
+
logger.error('Failed to refresh session models after restoring custom providers:', error)
|
|
650
|
+
}
|
|
644
651
|
summary.customProviders = countKeys(value)
|
|
645
652
|
}
|
|
646
653
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { sendJson, readJsonBody, decodeSegment } from '../utils/response.mjs'
|
|
2
2
|
import { readStore, writeStore, atomicUpdate, getComparable, getStoreRevision, readSessionStoreScoped, readSessionValue, writeSessionValueWithMetadata, deleteSessionWithMetadata, applySessionBatch, ensureStorage, dataDir, configDir, storageDir, cacheDir, logsDir } from '../storage.mjs'
|
|
3
3
|
import { AUTO_ARCHIVE_SETTINGS_KEY, archiveInactiveSessions, normalizeAutoArchiveSettings } from '../auto-archive.mjs'
|
|
4
|
+
import { refreshAllSessionModels } from '../agent-manager.mjs'
|
|
5
|
+
import { logger } from '../utils/logger.mjs'
|
|
4
6
|
import { directorySize } from '../utils/workspace.mjs'
|
|
5
7
|
import { isAuthenticatedAppClient } from '../access-policy.mjs'
|
|
6
8
|
import { getSessionIndexDiagnostics, markSessionIndexQueryFailure, querySessionIndexPage } from '../session-index-service.mjs'
|
|
@@ -293,6 +295,13 @@ export async function handleStorageApi(req, res, url, context = { isLocalRequest
|
|
|
293
295
|
|
|
294
296
|
if (req.method === 'DELETE' && parts.length === 3) {
|
|
295
297
|
await writeStore(store, {})
|
|
298
|
+
if (store === 'custom-providers') {
|
|
299
|
+
try {
|
|
300
|
+
await refreshAllSessionModels()
|
|
301
|
+
} catch (error) {
|
|
302
|
+
logger.error('Failed to refresh session models after clearing custom-providers:', error)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
296
305
|
sendJson(res, 200, { ok: true })
|
|
297
306
|
return
|
|
298
307
|
}
|
|
@@ -340,6 +349,13 @@ export async function handleStorageApi(req, res, url, context = { isLocalRequest
|
|
|
340
349
|
if (store === 'settings' && key === AUTO_ARCHIVE_SETTINGS_KEY && normalizeAutoArchiveSettings(body?.value).enabled) {
|
|
341
350
|
await archiveInactiveSessions()
|
|
342
351
|
}
|
|
352
|
+
if (store === 'custom-providers') {
|
|
353
|
+
try {
|
|
354
|
+
await refreshAllSessionModels()
|
|
355
|
+
} catch (error) {
|
|
356
|
+
logger.error('Failed to refresh session models after custom-providers update:', error)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
343
359
|
sendJson(res, 200, { ok: true })
|
|
344
360
|
return
|
|
345
361
|
}
|
|
@@ -355,6 +371,13 @@ export async function handleStorageApi(req, res, url, context = { isLocalRequest
|
|
|
355
371
|
delete data[key]
|
|
356
372
|
return data
|
|
357
373
|
})
|
|
374
|
+
if (store === 'custom-providers') {
|
|
375
|
+
try {
|
|
376
|
+
await refreshAllSessionModels()
|
|
377
|
+
} catch (error) {
|
|
378
|
+
logger.error('Failed to refresh session models after custom-providers key deletion:', error)
|
|
379
|
+
}
|
|
380
|
+
}
|
|
358
381
|
sendJson(res, 200, { ok: true })
|
|
359
382
|
return
|
|
360
383
|
}
|
|
@@ -1,9 +1,20 @@
|
|
|
1
|
-
|
|
1
|
+
// Keyed FIFO persistence queues. Each key owns one promise chain, so
|
|
2
|
+
// operations under the same key run strictly in order while operations under
|
|
3
|
+
// different keys run independently. The default (global) key serializes
|
|
4
|
+
// everything; per-session keys are used once SQLite session state is
|
|
5
|
+
// authoritative, where correctness relies on per-row revision CAS instead of
|
|
6
|
+
// global mutual exclusion. JSON-mirror mode and auto-archive keep the global
|
|
7
|
+
// key for their bucket-wide read-modify-write atomicity.
|
|
8
|
+
const persistenceQueues = new Map()
|
|
2
9
|
|
|
3
|
-
export function withSessionPersistenceLock(operation) {
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
10
|
+
export function withSessionPersistenceLock(operation, key = '') {
|
|
11
|
+
const previous = persistenceQueues.get(key) ?? Promise.resolve()
|
|
12
|
+
const result = previous.catch(() => undefined).then(operation)
|
|
13
|
+
const tail = result.then(() => undefined, () => undefined)
|
|
14
|
+
persistenceQueues.set(key, tail)
|
|
15
|
+
tail.then(() => {
|
|
16
|
+
// Drop the queue entry once it drains so idle keys do not linger.
|
|
17
|
+
if (persistenceQueues.get(key) === tail) persistenceQueues.delete(key)
|
|
18
|
+
})
|
|
8
19
|
return result
|
|
9
20
|
}
|
|
@@ -86,8 +86,10 @@ export async function exportSessionStateForBackup(options = {}) {
|
|
|
86
86
|
const integrity = repository.verifyIntegrity({ quickCheck: true })
|
|
87
87
|
if (!integrity.ok) throw new Error('Session state integrity verification failed during backup export')
|
|
88
88
|
const snapshot = repository.exportSnapshot()
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
// The lightweight integrity result has no digest (null); the count
|
|
90
|
+
// cross-check plus exportSnapshot's own digest computation stay.
|
|
91
|
+
if (snapshot.count !== integrity.count) {
|
|
92
|
+
throw new Error('Session state backup count verification failed')
|
|
91
93
|
}
|
|
92
94
|
return {
|
|
93
95
|
...snapshotValues(snapshot),
|