bitfolk 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -72,7 +72,7 @@ exec curl -sS --max-time 30 -X "$method" -H "Authorization: Bearer $BITFOLK_TOKE
|
|
|
72
72
|
`)){let[n,a]=t.split(` `);n&&Number(a)<i&&(X(e,[`update-ref`,`-d`,n]),r++)}}catch{}return r}const ya=36e5,ba=process.env.BITFOLK_FAST_POLL===`1`,xa=ba?3e3:3e4,Sa=ba?3e4:6e5,Ca=ba?2e3:15e3,wa=ba?5e3:6e4,Ta=ba?1e3:5e3,Ea={session_requested:`a session you asked for`,claimable_tasks:`tasks to claim`,my_tasks:`its tasks`,new_messages:`new messages`,review_waiting:`a review`,briefing_due:`a briefing`,setup_actions:`setup`,watch_fired:`a watch`},Da=e=>e.map(e=>Ea[e]??e.replace(/_/g,` `)).join(`, `)||`new work`;function Oa(e,t){if(e){if(e.pid)try{process.kill(-e.pid,t);return}catch{}e.kill(t)}}const ka=new WeakSet;function Aa(e,t=1e4){Oa(e,`SIGTERM`),setTimeout(()=>{e&&!ka.has(e)&&Oa(e,`SIGKILL`)},t).unref()}function ja(e){let t=Math.round(e/1e3);return t<60?`${t}s`:`${Math.floor(t/60)}m${String(t%60).padStart(2,`0`)}s`}function Ma(e){let t=[];e.costMicros!==null&&t.push(`$${(e.costMicros/1e6).toFixed(e.costMicros>0&&e.costMicros<1e4?4:2)}`);let n=e.inputTokens+e.outputTokens;return n&&t.push(n>=1e3?`${Math.round(n/1e3)}k tokens`:`${n} tokens`),t.join(`, `)}const Na={info:[()=>B(H.skip),e=>e],idle:[()=>B(H.idle),B],start:[()=>V.accent(H.dot),e=>e],ok:[()=>V.ok(H.ok),e=>e],fail:[()=>V.err(H.fail),V.err],warn:[()=>V.warn(H.warn),V.warn],progress:[()=>B(H.arrow),B]};function Pa(e,t,n){let r=(t.runStarts??[]).filter(e=>e>n-ya).sort((e,t)=>e-t),i=e.maxRunsPerHour-r.length;return{left:i,freesAt:i>0?n:(r[0]??n)+ya}}function Fa(e,t){if(e.command)return{runtime:`command`,command:e.command};if(t.startsWith(`custom:`)){let n=e.customTools?.find(e=>`custom:${e.name}`===t);if(!n)throw Error(`the team asked for ${t.slice(7)}, which isn't set up here (${C} add)`);return n.protocol===`acp`?{runtime:`acp`,acp:{...Pe(n.command),agent:t}}:{runtime:`command`,command:n.command}}let n=Te(t);if(n)return{runtime:n};let r=pr(t);if(r)return{runtime:`acp`,acp:r};throw Error(`this runner can't start ${t}`)}function Ia(e,t){let n=t.session.retry,r=n?.mode===`fix`&&n.branch?.startsWith(`bitfolk/builder-`)?n.branch.slice(8):null;return{id:t.builder.id,name:t.builder.name,slug:r&&Tt(r)===r?r:Tt(`builder-${t.session.id.slice(-8)}`),...Fa(e,t.session.runtime),api:e.api,token:t.builder.token,repo:e.repo,base:(n?.mode===`redo`&&/^[0-9a-f]{7,64}$/.test(n.base_sha??``)?n.base_sha:null)??e.base,maxUsdPerRun:e.maxUsdPerRun,maxRunsPerHour:e.maxRunsPerHour,timeoutMin:e.timeoutMin,permission:e.permission,model:e.model}}const Q=`machine`;function La(e=process.env){return Object.fromEntries(Object.entries(e).filter(([e])=>!/^npm_[a-z]/.test(e)&&e!==`INIT_CWD`&&e!==`NODE`))}function Ra(e=process.env,t=me){let n=(e.PATH??``).split(i).filter(Boolean);if(!t)return n;let r=n.findIndex(e=>/[\\/]node-gyp-bin$/.test(e));return(r>=0?n.slice(r+1):n).filter(e=>!/[\\/]_npx[\\/]/.test(e))}var za=class{config;state;#e=new Map;#t=new Map;#n=new Map;#r=new Map;#i;#a;#o;#s;#c;#l=!1;#u=null;#d=null;#f=0;#p=null;#m=0;#h=!1;#g;#_=null;#v=new Map;#y=0;#b=null;#x=null;#S={day:``,done:0,failed:0,stopped:0,costMicros:0,durationMs:0};constructor(e,t={}){this.config=e,this.state=Ct(),this.#i=t.out??(e=>process.stdout.write(e+`
|
|
73
73
|
`)),this.#a=t.now??Date.now,this.#o=t.progressMs??1e4,this.#s=t.checkpointMs??18e4,this.#c=t.acpCancelGraceMs,this.#g=!!t.liveFooter}#C(e){return this.state.agents[e.id]??={}}#w(e){let t=this.#r.get(e.id);return t||this.#r.set(e.id,t=new pt(e.api,e.oauth?()=>ar(e,{now:this.#a}):e.token)),t}#T(e,t,n=`info`,r){let i=new Date(this.#a()).toTimeString().slice(0,8),[a,o]=Na[n],s=r?()=>r:a,c=e?e.name:``,l=c?tn(c)(c):``,u=Kt(),d=on(o(t));if(u<60){this.#i(G(`${B(i)} ${s()} ${l}${l?` `:``}${d}`,u,4).join(`
|
|
74
74
|
`));return}let f=`${B(i)} ${s()} ${l}${` `.repeat(Math.max(0,12-U(c)))} `;this.#i(G(f+d,u,U(f)).join(`
|
|
75
|
-
`))}#E(){wt(this.state)}#D(e,t){this.#t.set(e.id,this.#a()+Math.max(1e3,t))}#O(e){let t=(this.#n.get(e.id)??0)+1;return this.#n.set(e.id,t),Math.min(Sa,Ca*2**(t-1))}stop(){if(!this.#l){this.#l=!0;for(let e of this.#e.values())this.#k(e,`owner`);this.#p?.()}}#k(e,t){if(e.cancel){let n=e.cancel(t);return n&&t===`owner`&&(e.stopped=!0),n}return t===`owner`&&(e.stopped=!0),Aa(e.child),!0}killAll(){for(let e of this.#e.values())e.stopped=!0,Oa(e.child,`SIGKILL`)}async#A(){if([...this.#e.values()].some(e=>e.sessionId&&!e.stopped))try{let e=await this.#z()?.peek();e&&this.#P(Q);for(let t of e?.cancel??[])this.#j(t)}catch{}}#j(e){for(let[t,n]of this.#e)if(!(n.sessionId!==e||n.stopped))return this.#T({id:t,name:`Builder`},`canceled from your team; stopping it`,`warn`),this.#k(n,`owner`),!0;return!1}#M(e,t){let n=[],r=null,i=JSON.stringify([null,null,null]),a=!1,o=!1,s=async()=>{let r=this.#z();if(a||o||!r)return;let s={input_tokens:t.inputTokens||null,output_tokens:t.outputTokens||null,cost_micros:t.costMicros},c=JSON.stringify(Object.values(s));if(n.length||c!==i){a=!0;try{let t=n.slice(),a=await r.progress(e,{lines:t,...s});if(a.status!==`error`){i=c;let e=new Set(t);for(;n.length&&e.has(n[0]);)n.shift()}a.status===`not_found`&&(o=!0),a.canceled&&!o&&this.#j(e)}finally{a=!1}}},c=setInterval(()=>void s(),this.#o);return{add(e){e&&e!==r&&(r=e,n.push(e),n.length>5&&n.shift())},stop(){o=!0,clearInterval(c)}}}get running(){return this.#e.size}status(){let e=[...this.#e.values()],t=e.map(e=>e.tally?.costMicros).filter(e=>typeof e==`number`),n=e.map(e=>e.startedAt).filter(e=>typeof e==`number`),r=[...this.#t.entries()].filter(([e])=>e!==Q||!this.#d).map(([,e])=>e);return{running:e.length,since:n.length?Math.min(...n):null,costMicros:t.length?t.reduce((e,t)=>e+t,0):null,nextCheckAt:this.#h||this.#m||!r.length?null:Math.min(...r),checking:this.#m>0,stopping:this.#l,sessions:e.map(e=>({name:e.name??``,tool:e.tool??``,title:e.title??``,startedAt:e.startedAt??e.joinedAt??this.#a(),costMicros:e.tally?.costMicros??null,activity:e.activity??null,activityAt:e.activityAt??null,stopping:e.stopped})).sort((e,t)=>e.startedAt-t.startedAt),queued:this.#y,waitingFor:this.#y?this.#b:null,capFreesAt:this.#x,lastCheckAt:this.#_,checkFailure:[...this.#v.values()].sort((e,t)=>e.since-t.since)[0]??null,signedOut:this.#d,runsThisHour:this.config.machine?this.config.machine.maxRunsPerHour-Pa(this.config.machine,this.state.agents[Q]??{},this.#a()).left:null,today:this.#N()}}#N(){let e=new Date(this.#a()).toDateString();return this.#S.day!==e&&(this.#S={day:e,done:0,failed:0,stopped:0,costMicros:0,durationMs:0}),this.#S}#P(e){this.#_=this.#a(),this.#v.delete(e)}#F(e,t,n){let r=this.#a(),i=t.message;this.#v.set(e,{message:i,since:this.#v.get(e)?.since??r,retryAt:r+n})}async#I(e){this.#m++;try{return await e()}finally{this.#m--}}async check(e){let t=this.#C(e);if(t.disabledReason||this.#e.has(e.id))return null;let{left:n,freesAt:r}=Pa(e,t,this.#a());if(n<=0)return this.#T(e,`hit ${e.maxRunsPerHour} runs this hour; next check at ${new Date(r).toTimeString().slice(0,5)}`,`warn`),this.#t.set(e.id,r),null;let i;try{i=await this.#I(()=>this.#w(e).peek(t.etag))}catch(n){if(n instanceof q&&n.ended)return t.disabledReason=`its sign-in ended (unlinked on your team, or unused for 90 days)`,this.#T(e,`stopped: ${t.disabledReason}. Copy a new token from its page on your team and run \`${C} add <token>\`.`,`fail`),this.#E(),null;if(n instanceof M&&n.status===401){let r=n.code===`agent_removed`;return t.disabledReason=r?`its owner removed it from Bitfolk`:`token rejected (revoked, or the agent was archived)`,this.#T(e,r?`stopped: ${t.disabledReason}. Run \`${C} rm "${e.name}"\` to forget it.`:`stopped: ${t.disabledReason}. Copy a new token from its page on your team and run \`${C} add <token>\` (it keeps your options).`,`fail`),this.#E(),null}let r=this.#O(e);return this.#T(e,`peek failed (${n.message}); retrying in ${ja(r)}`,`warn`),this.#D(e,r),this.#F(e.id,n,Math.max(1e3,r)),null}return this.#n.delete(e.id),this.#P(e.id),i?(t.etag=i.etag,t.pollMs=i.next_poll_after_s*1e3,this.#E(),i.tier===`silent`||!i.has_work?(this.#D(e,i.next_poll_after_s*1e3),null):{done:this.#L(e,i)}):(this.#D(e,t.pollMs??wa),null)}#L(e,t,n){let r={child:null,done:Promise.resolve(null),stopped:!1,sessionId:n?.session.id,name:e.name,tool:n?.session.runtime??e.acp?.agent??e.runtime,title:an(n?n.session.task?.title??n.session.goal?.title??`a goal`:`woke for ${Da(t.reasons)}`),joinedAt:this.#a()},i=this.#V(e,t,r,n);return n?.session.retry?.mode===`fix`&&(i=i.then(i=>!i?.resumed||i.status!==`failed`||i.durationMs>=2e4||r.stopped||this.#l?i:(this.#T(e,`couldn't pick up the earlier conversation; starting fresh on the same branch`,`warn`),this.#V(e,t,r,n,{fresh:!0})))),r.done=(n?i.then(t=>this.#B(e,n,t)):i).finally(()=>{this.#e.delete(e.id),this.#p?.()}),this.#e.set(e.id,r),r.done}#R(e,t,n,r){let i=``,a=null,o=()=>a??=(async()=>{let a=this.#z();if(!a)return;let o=null;try{o=ga(t.dir,e)}catch{}let s={branch:t.branch??null,base_sha:n,head_sha:o?.head??null,checkpoint_sha:o?.checkpoint??null,tool_session_id:r.sessionId??null},c=JSON.stringify(s);c!==i&&await a.checkpoint(e,s)&&(i=c)})().finally(()=>{a=null}),s=setInterval(()=>void o(),this.#s);return o(),{async final(){clearInterval(s),await a,await o()},stop:()=>clearInterval(s)}}#z(){let e=this.config.machine;return!e||this.#d?null:this.#u??=new mt(e.api,()=>ar(e,{now:this.#a}))}async checkMachine(){let e=this.config.machine,t=this.#z();if(!e||!t)return[];let n=this.state.agents[Q]??={},r={id:Q,name:e.name},i;try{i=await this.#I(()=>t.peek())}catch(e){if(e instanceof q&&e.ended||e instanceof M&&e.status===401)return this.#d=`this machine was unlinked, or its sign-in ended`,this.#T(r,`stopped: ${this.#d}. Run \`${C} login\` to link it again.`,`fail`),[];let t=this.#O(r);return this.#T(r,`check-in failed (${e.message}); retrying in ${ja(t)}`,`warn`),this.#t.set(Q,this.#a()+t),this.#F(Q,e,t),[]}this.#n.delete(r.id),this.#P(Q);for(let e of i.cancel??[])this.#j(e);let a=Math.max(1e3,i.next_poll_after_s*1e3);this.#t.set(Q,this.#a()+a);let o=()=>{[...this.#e.values()].some(e=>e.sessionId&&!e.stopped)&&this.#t.set(Q,Math.min(this.#t.get(Q)??1/0,this.#a()+Ta))},s=[],c=i.queued;for(this.#b=null,this.#x=null;c>0&&!this.#l;c--){if(this.#e.size>=this.config.maxConcurrent){this.#b=`slot`;break}let{left:i,freesAt:a}=Pa(e,n,this.#a());if(i<=0){this.#b=`cap`,this.#x=a,this.#f!==a&&(this.#f=a,this.#T(r,`hit ${e.maxRunsPerHour} sessions this hour; the rest wait until ${new Date(a).toTimeString().slice(0,5)}`,`warn`)),this.#t.set(Q,a);break}let o;try{o=await this.#I(()=>t.claim({runtimes:e.runtimes,timeout_min:e.timeoutMin}))}catch(e){this.#T(r,`couldn't take a session (${e.message})`,`warn`);break}if(!o){c=0;break}this.#y=Math.max(0,this.#y-1),n.runStarts=[...(n.runStarts??[]).filter(e=>e>this.#a()-ya),this.#a()],this.#E();let l;try{l=Ia(e,o)}catch(e){this.#T(r,`can't run a session: ${e.message}`,`fail`),await t.finish(o.session.id,{status:`failed`,summary:null,error:e.message}).catch(()=>{});continue}let u=an(o.session.task?.title??o.session.goal?.title??`a goal`);this.#T(l,`joined for a session: ${V.bold(u)} ${B(`·`)} ${V.tool(sn(J(o.session.runtime)))}`,`start`,si(o.session.runtime));let d=await new pt(l.api,l.token).peek().catch(()=>null),f={has_work:!0,tier:`full`,reasons:[`session_requested`,...(d?.reasons??[]).filter(e=>e!==`setup_actions`)],briefing_goal_id:o.session.goal?.id??null,etag:d?.etag??``,next_poll_after_s:60,rules:d?.rules};s.push({done:this.#L(l,f,o)})}return this.#y=this.#b?Math.max(0,c):0,o(),s}async#B(e,t,n){let r=n?.status===`succeeded`?`succeeded`:n?.status===`interrupted`?`canceled`:`failed`,i=null;if(n){let t=n.work?.startSha??null,r={commits:[],commit_count:0,files_changed:0,insertions:0,deletions:0};if(n.work?.branch&&t)try{e.repo&&ia(n.work.dir,e.repo),r=pa(n.work.dir,t,n.work.branch)}catch{}i={base:t?.slice(0,12)??null,...r,duration_ms:n.durationMs}}try{await this.#z()?.finish(t.session.id,{status:r,summary:n?.tally.summary??null,error:n?n.status===`succeeded`?null:n.tally.error??n.status:`the session couldn't start on this machine`,branch:n?.work?.branch??null,result:i,input_tokens:n?n.tally.inputTokens:null,output_tokens:n?n.tally.outputTokens:null,cost_micros:n?.tally.costMicros??null,model:((e.runtime===`acp`?n?.tally.model??e.model:e.model??n?.tally.model)??null)?.slice(0,100)??null}),i?.commit_count&&this.#T(e,`left ${V.bold(`${i.commit_count} commit${i.commit_count===1?``:`s`}`)} on ${V.tool(n.work.branch)} ${B(`(${i.files_changed} files)`)}`,`ok`)}catch(t){this.#T(e,`couldn't report the session's end (${t.message}); Bitfolk times it out`,`warn`)}return this.#T(e,`left the team`,`idle`),da(e),e.repo&&va(e.repo),delete this.state.agents[e.id],this.#E(),n}async#V(e,t,r,a,o={}){let c=this.#C(e),l=this.#w(e),u=we[e.runtime],d=e.runtime===`acp`?e.acp:void 0;if(e.runtime===`acp`&&!d)throw Error(`${e.name} has runtime acp but no ACP command`);let f=this.#a(),m;try{m=ca(e)}catch(t){return c.etag=void 0,this.#T(e,`can't prepare its workspace: ${t.message}`,`fail`),this.#D(e,this.#O(e)),null}let h=m.branch?fa(m.dir):null,g=a?.session.retry??null,_=!!(g?.mode===`fix`&&m.created&&m.branch&&g.checkpoint_sha&&_a(m.dir,g.checkpoint_sha));_&&this.#T(e,`put back the earlier session's uncommitted work on ${V.tool(String(m.branch))}`,`ok`);let v=null;try{v=await l.runStart({client_run_id:fe(),host:se(),runtime:d?d.agent:e.runtime,reasons:t.reasons,tier:t.tier})}catch(n){if(n instanceof M&&n.status===409)return this.#T(e,`is paused by its owner; not starting`,`idle`),this.#D(e,t.next_poll_after_s*1e3||(ba?wa:6e5)),null;this.#T(e,`couldn't record the run start (${n.message}); running anyway`,`warn`)}v||await l.status(`working`).catch(()=>{});let b=o.fresh?null:g?.mode===`fix`?u.resumes!==!1&&g.tool_session_id?g.tool_session_id:null:u.resumes!==!1&&!m.created&&c.sessionId&&c.sessionAt&&f-c.sessionAt<432e5?c.sessionId:null,x=!!m.branch&&!!u.runnerCommits?.(e),ee=ra(t,{agentName:e.name,api:e.api,branch:m.branch,runnerCommits:x,resumed:!!b,session:a?{goal:a.session.goal?.title??null,task:a.session.task?.title??null,note:a.session.note,goalDone:a.session.goal?.done_definition??null,taskSpec:a.session.task?.spec??null,asks:a.session.asks??[],requestedBy:a.session.requested_by??`owner`,retry:g?{mode:g.mode,branch:g.branch,summary:g.summary,error:g.error,status:g.status,restored:_}:void 0}:void 0}),te=u.launch(e,ee,b);c.runStarts=[...(c.runStarts??[]).filter(e=>e>f-ya),f],this.#E(),this.#T(e,`woke ${B(`for ${Da(t.reasons)}`)} → ${V.tool(sn(d?`${J(d.agent)} over ACP`:e.runtime))}${b?B(`, resuming`):``}`,`start`);let ne=s(N(),`logs`,e.slug);y(ne,{recursive:!0,mode:448});let ie=s(ne,`${new Date(f).toISOString().replace(/[:.]/g,`-`)}.jsonl`),ae=new Set([e.token]),oe=e=>[...ae].reduce((e,t)=>e.split(t).join(`[token]`),e),ce=(e,t)=>p(ie,JSON.stringify({t:this.#a(),stream:e,line:oe(t)})+`
|
|
75
|
+
`))}#E(){wt(this.state)}#D(e,t){let n=this.#a()+Math.max(1e3,t);return this.#t.set(e.id,n),n}#O(e){let t=(this.#n.get(e.id)??0)+1;return this.#n.set(e.id,t),Math.min(Sa,Ca*2**(t-1))}stop(){if(!this.#l){this.#l=!0;for(let e of this.#e.values())this.#k(e,`owner`);this.#p?.()}}#k(e,t){if(e.cancel){let n=e.cancel(t);return n&&t===`owner`&&(e.stopped=!0),n}return t===`owner`&&(e.stopped=!0),Aa(e.child),!0}killAll(){for(let e of this.#e.values())e.stopped=!0,Oa(e.child,`SIGKILL`)}async#A(){if([...this.#e.values()].some(e=>e.sessionId&&!e.stopped))try{let e=await this.#z()?.peek();e&&this.#P(Q);for(let t of e?.cancel??[])this.#j(t)}catch{}}#j(e){for(let[t,n]of this.#e)if(!(n.sessionId!==e||n.stopped))return this.#T({id:t,name:`Builder`},`canceled from your team; stopping it`,`warn`),this.#k(n,`owner`),!0;return!1}#M(e,t){let n=[],r=null,i=JSON.stringify([null,null,null]),a=!1,o=!1,s=async()=>{let r=this.#z();if(a||o||!r)return;let s={input_tokens:t.inputTokens||null,output_tokens:t.outputTokens||null,cost_micros:t.costMicros},c=JSON.stringify(Object.values(s));if(n.length||c!==i){a=!0;try{let t=n.slice(),a=await r.progress(e,{lines:t,...s});if(a.status!==`error`){i=c;let e=new Set(t);for(;n.length&&e.has(n[0]);)n.shift()}a.status===`not_found`&&(o=!0),a.canceled&&!o&&this.#j(e)}finally{a=!1}}},c=setInterval(()=>void s(),this.#o);return{add(e){e&&e!==r&&(r=e,n.push(e),n.length>5&&n.shift())},stop(){o=!0,clearInterval(c)}}}get running(){return this.#e.size}status(){let e=[...this.#e.values()],t=e.map(e=>e.tally?.costMicros).filter(e=>typeof e==`number`),n=e.map(e=>e.startedAt).filter(e=>typeof e==`number`),r=[...this.#t.entries()].filter(([e])=>e!==Q||!this.#d).map(([,e])=>e);return{running:e.length,since:n.length?Math.min(...n):null,costMicros:t.length?t.reduce((e,t)=>e+t,0):null,nextCheckAt:this.#h||this.#m||!r.length?null:Math.min(...r),checking:this.#m>0,stopping:this.#l,sessions:e.map(e=>({name:e.name??``,tool:e.tool??``,title:e.title??``,startedAt:e.startedAt??e.joinedAt??this.#a(),costMicros:e.tally?.costMicros??null,activity:e.activity??null,activityAt:e.activityAt??null,stopping:e.stopped})).sort((e,t)=>e.startedAt-t.startedAt),queued:this.#y,waitingFor:this.#y?this.#b:null,capFreesAt:this.#x,lastCheckAt:this.#_,checkFailure:[...this.#v.values()].sort((e,t)=>e.since-t.since)[0]??null,signedOut:this.#d,runsThisHour:this.config.machine?this.config.machine.maxRunsPerHour-Pa(this.config.machine,this.state.agents[Q]??{},this.#a()).left:null,today:this.#N()}}#N(){let e=new Date(this.#a()).toDateString();return this.#S.day!==e&&(this.#S={day:e,done:0,failed:0,stopped:0,costMicros:0,durationMs:0}),this.#S}#P(e){this.#_=this.#a(),this.#v.delete(e)}#F(e,t,n){let r=t.message;this.#v.set(e,{message:r,since:this.#v.get(e)?.since??this.#a(),retryAt:n})}async#I(e){this.#m++;try{return await e()}finally{this.#m--}}async check(e){let t=this.#C(e);if(t.disabledReason||this.#e.has(e.id))return null;let{left:n,freesAt:r}=Pa(e,t,this.#a());if(n<=0)return this.#T(e,`hit ${e.maxRunsPerHour} runs this hour; next check at ${new Date(r).toTimeString().slice(0,5)}`,`warn`),this.#t.set(e.id,r),null;let i;try{i=await this.#I(()=>this.#w(e).peek(t.etag))}catch(n){if(n instanceof q&&n.ended)return t.disabledReason=`its sign-in ended (unlinked on your team, or unused for 90 days)`,this.#T(e,`stopped: ${t.disabledReason}. Copy a new token from its page on your team and run \`${C} add <token>\`.`,`fail`),this.#E(),null;if(n instanceof M&&n.status===401){let r=n.code===`agent_removed`;return t.disabledReason=r?`its owner removed it from Bitfolk`:`token rejected (revoked, or the agent was archived)`,this.#T(e,r?`stopped: ${t.disabledReason}. Run \`${C} rm "${e.name}"\` to forget it.`:`stopped: ${t.disabledReason}. Copy a new token from its page on your team and run \`${C} add <token>\` (it keeps your options).`,`fail`),this.#E(),null}let r=this.#O(e);return this.#T(e,`peek failed (${n.message}); retrying in ${ja(r)}`,`warn`),this.#F(e.id,n,this.#D(e,r)),null}return this.#n.delete(e.id),this.#P(e.id),i?(t.etag=i.etag,t.pollMs=i.next_poll_after_s*1e3,this.#E(),i.tier===`silent`||!i.has_work?(this.#D(e,i.next_poll_after_s*1e3),null):{done:this.#L(e,i)}):(this.#D(e,t.pollMs??wa),null)}#L(e,t,n){let r={child:null,done:Promise.resolve(null),stopped:!1,sessionId:n?.session.id,name:e.name,tool:n?.session.runtime??e.acp?.agent??e.runtime,title:an(n?n.session.task?.title??n.session.goal?.title??`a goal`:`woke for ${Da(t.reasons)}`),joinedAt:this.#a()},i=this.#V(e,t,r,n);return n?.session.retry?.mode===`fix`&&(i=i.then(i=>!i?.resumed||i.status!==`failed`||i.durationMs>=2e4||r.stopped||this.#l?i:(this.#T(e,`couldn't pick up the earlier conversation; starting fresh on the same branch`,`warn`),this.#V(e,t,r,n,{fresh:!0})))),r.done=(n?i.then(t=>this.#B(e,n,t)):i).finally(()=>{this.#e.delete(e.id),this.#p?.()}),this.#e.set(e.id,r),r.done}#R(e,t,n,r){let i=``,a=null,o=()=>a??=(async()=>{let a=this.#z();if(!a)return;let o=null;try{o=ga(t.dir,e)}catch{}let s={branch:t.branch??null,base_sha:n,head_sha:o?.head??null,checkpoint_sha:o?.checkpoint??null,tool_session_id:r.sessionId??null},c=JSON.stringify(s);c!==i&&await a.checkpoint(e,s)&&(i=c)})().finally(()=>{a=null}),s=setInterval(()=>void o(),this.#s);return o(),{async final(){clearInterval(s),await a,await o()},stop:()=>clearInterval(s)}}#z(){let e=this.config.machine;return!e||this.#d?null:this.#u??=new mt(e.api,()=>ar(e,{now:this.#a}))}async checkMachine(){let e=this.config.machine,t=this.#z();if(!e||!t)return[];let n=this.state.agents[Q]??={},r={id:Q,name:e.name},i;try{i=await this.#I(()=>t.peek())}catch(e){if(e instanceof q&&e.ended||e instanceof M&&e.status===401)return this.#d=`this machine was unlinked, or its sign-in ended`,this.#T(r,`stopped: ${this.#d}. Run \`${C} login\` to link it again.`,`fail`),[];let t=this.#O(r);this.#T(r,`check-in failed (${e.message}); retrying in ${ja(t)}`,`warn`);let n=this.#a()+t;return this.#t.set(Q,n),this.#F(Q,e,n),[]}this.#n.delete(r.id),this.#P(Q);for(let e of i.cancel??[])this.#j(e);let a=Math.max(1e3,i.next_poll_after_s*1e3);this.#t.set(Q,this.#a()+a);let o=()=>{[...this.#e.values()].some(e=>e.sessionId&&!e.stopped)&&this.#t.set(Q,Math.min(this.#t.get(Q)??1/0,this.#a()+Ta))},s=[],c=i.queued;for(this.#b=null,this.#x=null;c>0&&!this.#l;c--){if(this.#e.size>=this.config.maxConcurrent){this.#b=`slot`;break}let{left:i,freesAt:a}=Pa(e,n,this.#a());if(i<=0){this.#b=`cap`,this.#x=a,this.#f!==a&&(this.#f=a,this.#T(r,`hit ${e.maxRunsPerHour} sessions this hour; the rest wait until ${new Date(a).toTimeString().slice(0,5)}`,`warn`)),this.#t.set(Q,a);break}let o;try{o=await this.#I(()=>t.claim({runtimes:e.runtimes,timeout_min:e.timeoutMin}))}catch(e){this.#T(r,`couldn't take a session (${e.message})`,`warn`);break}if(!o){c=0;break}this.#y=Math.max(0,this.#y-1),n.runStarts=[...(n.runStarts??[]).filter(e=>e>this.#a()-ya),this.#a()],this.#E();let l;try{l=Ia(e,o)}catch(e){this.#T(r,`can't run a session: ${e.message}`,`fail`),await t.finish(o.session.id,{status:`failed`,summary:null,error:e.message}).catch(()=>{});continue}let u=an(o.session.task?.title??o.session.goal?.title??`a goal`);this.#T(l,`joined for a session: ${V.bold(u)} ${B(`·`)} ${V.tool(sn(J(o.session.runtime)))}`,`start`,si(o.session.runtime));let d=await new pt(l.api,l.token).peek().catch(()=>null),f={has_work:!0,tier:`full`,reasons:[`session_requested`,...(d?.reasons??[]).filter(e=>e!==`setup_actions`)],briefing_goal_id:o.session.goal?.id??null,etag:d?.etag??``,next_poll_after_s:60,rules:d?.rules};s.push({done:this.#L(l,f,o)})}return this.#y=this.#b?Math.max(0,c):0,o(),s}async#B(e,t,n){let r=n?.status===`succeeded`?`succeeded`:n?.status===`interrupted`?`canceled`:`failed`,i=null;if(n){let t=n.work?.startSha??null,r={commits:[],commit_count:0,files_changed:0,insertions:0,deletions:0};if(n.work?.branch&&t)try{e.repo&&ia(n.work.dir,e.repo),r=pa(n.work.dir,t,n.work.branch)}catch{}i={base:t?.slice(0,12)??null,...r,duration_ms:n.durationMs}}try{await this.#z()?.finish(t.session.id,{status:r,summary:n?.tally.summary??null,error:n?n.status===`succeeded`?null:n.tally.error??n.status:`the session couldn't start on this machine`,branch:n?.work?.branch??null,result:i,input_tokens:n?n.tally.inputTokens:null,output_tokens:n?n.tally.outputTokens:null,cost_micros:n?.tally.costMicros??null,model:((e.runtime===`acp`?n?.tally.model??e.model:e.model??n?.tally.model)??null)?.slice(0,100)??null}),i?.commit_count&&this.#T(e,`left ${V.bold(`${i.commit_count} commit${i.commit_count===1?``:`s`}`)} on ${V.tool(n.work.branch)} ${B(`(${i.files_changed} files)`)}`,`ok`)}catch(t){this.#T(e,`couldn't report the session's end (${t.message}); Bitfolk times it out`,`warn`)}return this.#T(e,`left the team`,`idle`),da(e),e.repo&&va(e.repo),delete this.state.agents[e.id],this.#E(),n}async#V(e,t,r,a,o={}){let c=this.#C(e),l=this.#w(e),u=we[e.runtime],d=e.runtime===`acp`?e.acp:void 0;if(e.runtime===`acp`&&!d)throw Error(`${e.name} has runtime acp but no ACP command`);let f=this.#a(),m;try{m=ca(e)}catch(t){return c.etag=void 0,this.#T(e,`can't prepare its workspace: ${t.message}`,`fail`),this.#D(e,this.#O(e)),null}let h=m.branch?fa(m.dir):null,g=a?.session.retry??null,_=!!(g?.mode===`fix`&&m.created&&m.branch&&g.checkpoint_sha&&_a(m.dir,g.checkpoint_sha));_&&this.#T(e,`put back the earlier session's uncommitted work on ${V.tool(String(m.branch))}`,`ok`);let v=null;try{v=await l.runStart({client_run_id:fe(),host:se(),runtime:d?d.agent:e.runtime,reasons:t.reasons,tier:t.tier})}catch(n){if(n instanceof M&&n.status===409)return this.#T(e,`is paused by its owner; not starting`,`idle`),this.#D(e,t.next_poll_after_s*1e3||(ba?wa:6e5)),null;this.#T(e,`couldn't record the run start (${n.message}); running anyway`,`warn`)}v||await l.status(`working`).catch(()=>{});let b=o.fresh?null:g?.mode===`fix`?u.resumes!==!1&&g.tool_session_id?g.tool_session_id:null:u.resumes!==!1&&!m.created&&c.sessionId&&c.sessionAt&&f-c.sessionAt<432e5?c.sessionId:null,x=!!m.branch&&!!u.runnerCommits?.(e),ee=ra(t,{agentName:e.name,api:e.api,branch:m.branch,runnerCommits:x,resumed:!!b,session:a?{goal:a.session.goal?.title??null,task:a.session.task?.title??null,note:a.session.note,goalDone:a.session.goal?.done_definition??null,taskSpec:a.session.task?.spec??null,asks:a.session.asks??[],requestedBy:a.session.requested_by??`owner`,retry:g?{mode:g.mode,branch:g.branch,summary:g.summary,error:g.error,status:g.status,restored:_}:void 0}:void 0}),te=u.launch(e,ee,b);c.runStarts=[...(c.runStarts??[]).filter(e=>e>f-ya),f],this.#E(),this.#T(e,`woke ${B(`for ${Da(t.reasons)}`)} → ${V.tool(sn(d?`${J(d.agent)} over ACP`:e.runtime))}${b?B(`, resuming`):``}`,`start`);let ne=s(N(),`logs`,e.slug);y(ne,{recursive:!0,mode:448});let ie=s(ne,`${new Date(f).toISOString().replace(/[:.]/g,`-`)}.jsonl`),ae=new Set([e.token]),oe=e=>[...ae].reduce((e,t)=>e.split(t).join(`[token]`),e),ce=(e,t)=>p(ie,JSON.stringify({t:this.#a(),stream:e,line:oe(t)})+`
|
|
76
76
|
`,{mode:384}),ue=ta(),de=or(e),pe=e.oauth?setInterval(()=>{e.oauth.expiresAt-this.#a()>6e5||ar(e,{now:this.#a}).then(()=>{ae.add(e.token),or(e)}).catch(t=>this.#T(e,`couldn't renew its sign-in mid-run: ${t.message}`,`warn`))},6e4):null,S=ge();r.tally=S,r.startedAt=this.#a();let C={...La(),PATH:[ue,...Ra(),...lr()].join(i),...d?{NO_BROWSER:`1`}:{BITFOLK_TOKEN:e.token},BITFOLK_TOKEN_FILE:de,BITFOLK_API:e.api.replace(/\/+$/,``),BITFOLK_AGENT:e.name,...v?{BITFOLK_RUN_ID:v.id}:{}};d&&delete C.BITFOLK_TOKEN;for(let[e,t]of Object.entries(C))t&&t.length>=12&&/KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|AUTH/i.test(e)&&e!==`BITFOLK_TOKEN_FILE`&&ae.add(t);let me=a?this.#M(a.session.id,S):null,he=a&&m.branch?this.#R(a.session.id,m,h,S):null,w=null,_e=t=>{let n=t&&an(oe(t)).trim();me?.add(n),n&&n!==w&&(w=n,r.activity=n,r.activityAt=this.#a(),this.#T(e,n,`progress`))},T=null,E=null,D=!1,ve,ye=null;try{ve=await new Promise(t=>{let i=n(te.bin,te.args,{cwd:m.dir,env:C,stdio:[`pipe`,`pipe`,`pipe`],detached:!0});if(r.child=i,i.stdin.on(`error`,()=>{}),d){let t=new ut({stdin:i.stdin,stdout:i.stdout,agent:{...e,acp:d},label:J(d.agent),cwd:m.dir,home:N(),packet:ee,tally:S,clientVersion:j,log:ce,activity:_e,kill:()=>{ka.has(i)||Aa(i)},cancelGraceMs:this.#c});r.cancel=e=>t.cancel(e),ye=t.run()}else i.stdin.end(te.stdin),le({input:i.stdout}).on(`line`,e=>{ce(`out`,e),u.parse(e,S),u.activity&&_e(u.activity(e,m.dir))});(this.#l||r.stopped)&&this.#k(r,`owner`),le({input:i.stderr}).on(`line`,e=>{ce(`err`,e),e.trim()&&(E=oe(e.trim()).slice(0,300))});let a=setTimeout(()=>{D=!0,this.#k(r,`timeout`)||(D=!1)},e.timeoutMin*6e4);i.on(`error`,e=>{T=e.code===`ENOENT`?`${te.bin} isn't installed or isn't on PATH`:e.message}),i.on(`close`,e=>{ka.add(i),clearTimeout(a),t({code:e})})})}finally{me?.stop(),he?.stop(),pe&&clearInterval(pe)}let be=ye?await ye:null;r.cancel=void 0,u.afterExit?.(te,S),re(de,{force:!0});let O=this.#a()-f,k,A;if(be?(k=r.stopped?`interrupted`:S.overBudget?`over_budget`:T||D||!be.ok?`failed`:`succeeded`,A=T??(k===`over_budget`?S.error:D&&k===`failed`?`stopped after ${e.timeoutMin} minutes`:k===`failed`?be.exited?E??`${be.error??`the tool exited`}${ve.code===null?``:` (exit ${ve.code})`}`:be.error??E??`the turn failed`:null),k===`failed`&&(S.error=A)):(k=r.stopped?`interrupted`:S.overBudget?`over_budget`:T||D||ve.code!==0||S.isError?`failed`:`succeeded`,A=T??(D?`stopped after ${e.timeoutMin} minutes`:k===`failed`?S.error??E??`exit ${ve.code}`:null)),k===`failed`&&!S.error&&(S.error=A),x&&!T){let t=`${e.name}: ${S.summary??`work from a Bitfolk run`}`,n=`Bitfolk run${v?` ${v.id}`:``}, ${k}.`;try{e.repo&&m.branch&&ia(m.dir,e.repo);let r=la(m.dir,`${t.length>72?t.slice(0,71)+`…`:t}\n\n${n}`);r&&this.#T(e,`committed its changes as ${V.bold(r)} on ${V.tool(String(m.branch))}`,`ok`)}catch(t){this.#T(e,`couldn't commit its changes: ${t.message.split(`
|
|
77
77
|
`)[0]}`,`warn`)}}await he?.final().catch(()=>{}),k===`failed`&&b&&O<2e4?(c.sessionId=void 0,c.etag=void 0):S.sessionId&&(c.sessionId=S.sessionId,c.sessionAt=this.#a()),(k===`failed`||k===`interrupted`)&&(c.etag=void 0),this.#E();let xe={status:k,exit_code:be?be.ok&&k===`succeeded`?0:null:ve.code,session_id:S.sessionId,input_tokens:S.inputTokens,output_tokens:S.outputTokens,cost_micros:S.costMicros,summary:S.summary,error:A};v?await l.runFinish(v.id,xe).catch(async()=>{await new Promise(e=>setTimeout(e,2e3)),await l.runFinish(v.id,xe).catch(t=>this.#T(e,`couldn't record the run end: ${t.message}`,`warn`))}):await l.status(`idle`).catch(()=>{});let Se=this.#N();k===`succeeded`?Se.done++:k===`interrupted`?Se.stopped++:Se.failed++,Se.costMicros+=S.costMicros??0,Se.durationMs+=O;let Ce=Ma(S),Te=k===`succeeded`?V.ok(V.bold(k)):k===`failed`?V.err(V.bold(k)):V.warn(V.bold(k));return this.#T(e,[`${Te} in ${ja(O)}`,...Ce?Ce.split(`, `).map(e=>e.startsWith(`$`)?V.money(e):e):[]].join(` ${B(`·`)} `)+`${A?` ${V.err(`(${an(A)})`)}`:``}${S.summary?` ${B(`—`)} ${an(S.summary)}`:``}`,k===`succeeded`?`ok`:k===`failed`?`fail`:`warn`),k===`failed`?this.#D(e,this.#O(e)):(this.#n.delete(e.id),this.#D(e,xa)),{agent:e.name,status:k,tally:S,durationMs:O,work:{dir:m.dir,branch:m.branch,startSha:h},resumed:!!b}}async run(e={}){this.#h=!!e.once;let t=this.config.agents.filter(e=>!this.#C(e).disabledReason);for(let e of this.config.agents){let t=this.#C(e).disabledReason;t&&this.#T(e,`skipped: ${t}`,`idle`)}let n=this.config.machine;if(t.length===0&&!n)return this.#T(null,`nothing to run. Link this computer with \`${C} login\`.`,`warn`),[];if(e.once){let e=[];if(n){let t=await this.checkMachine();t.length===0&&!this.#d&&this.#T({name:n.name},`no sessions waiting`,`idle`),e.push(...t.map(e=>e.done))}for(let n of t){for(;this.#e.size>=this.config.maxConcurrent;)await Promise.race([...this.#e.values()].map(e=>e.done));let t=await this.check(n);t?e.push(t.done):this.#e.has(n.id)||this.#T(n,`nothing to do`,`idle`)}let r=n?setInterval(()=>void this.#A(),Ta):null;try{return(await Promise.all(e)).filter(e=>e!==null)}finally{r&&clearInterval(r)}}let r=[],i=[n?`${n.name} for sessions`:null,t.length?`${t.length} agent${t.length===1?``:`s`}`:null].filter(Boolean).join(` and `);for(this.#T(null,`watching ${V.bold(i)} ${B(`(up to ${this.config.maxConcurrent} at once).${this.#g?``:` Ctrl-C to stop.`}`)}`,`start`);!this.#l;){let e=this.#a();if(n&&!this.#d&&(this.#t.get(Q)??0)<=e)for(let e of await this.checkMachine())e.done.then(e=>e&&r.push(e));for(let n of t){if(this.#l||this.#e.size>=this.config.maxConcurrent)break;this.#e.has(n.id)||this.#C(n).disabledReason||(this.#t.get(n.id)??0)>e||(await this.check(n))?.done.then(e=>e&&r.push(e))}let i=Math.min(...t.map(t=>this.#t.get(t.id)??e),n&&!this.#d?this.#t.get(Q)??e:1/0),a=Math.max(1e3,Math.min(6e4,i-this.#a()));await new Promise(e=>{let t=setTimeout(e,a);this.#p=()=>{clearTimeout(t),e()}}),this.#p=null}return await Promise.all([...this.#e.values()].map(e=>e.done)),r}};const Ba=e=>Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,e);function Va(e){for(let t=0;t<20;t++){let t;try{t=x(e,`utf8`).trim()}catch{return null}if(t)return Number(t)||0;Ba(25)}return 0}function Ha(e){if(!e||e===process.pid)return!1;try{return process.kill(e,0),!0}catch(e){return e.code===`EPERM`}}function Ua(){let e=Va(s(N(),`runner.pid`));return e&&Ha(e)?e:null}function Wa(){let e=s(N(),`runner.pid`);y(N(),{recursive:!0,mode:448});let t=String(process.pid),n=()=>{try{x(e,`utf8`).trim()===t&&re(e,{force:!0})}catch{}};for(let r=0;r<3;r++){try{return ae(e,t,{mode:384,flag:`wx`}),n}catch(e){if(e.code!==`EEXIST`)throw e}let r=Va(e);if(r===null)continue;if(Ha(r))throw Error(`a runner is already going (pid ${r}). Stop it first, or see \`${C} status\`.`);let i=`${e}.${process.pid}.tmp`;if(ae(i,t,{mode:384}),ne(i,e),Ba(25),Va(e)===process.pid)return n}throw Error(`another runner is starting at the same moment. Try again, or see \`${C} status\`.`)}function Ga(e){let t;try{t=JSON.parse(e)}catch{return null}if(t.msg===`started tunnel`)return{kind:`started`};let n=typeof t.err==`string`&&t.err!==`<nil>`?t.err:null;return t.lvl===`crit`||t.lvl===`eror`&&n?{kind:`error`,message:String(n??t.msg??`ngrok failed`).replace(/\s+/g,` `).replace(/\bERR_NGROK_\d+\b.*$/,``).trim().slice(0,240)||`ngrok failed`}:null}function Ka(e,t=`127.0.0.1`){return new Promise(n=>{let r=S({port:e,host:t}),i=e=>{r.destroy(),n(e)};r.setTimeout(1500,()=>i(!1)),r.once(`connect`,()=>i(!0)),r.once(`error`,()=>i(!1))})}const qa={stopped_in_town:`Someone stopped it from the team's Settings.`,replaced:`It was shared again from this computer.`,machine_unlinked:`This computer was unlinked from the team.`,went_quiet:`This computer stopped checking in for a few minutes.`};async function Ja(e,t,r){let i=dr(`ngrok`);if(!i)throw Error(`sharing uses the ngrok program. Install it (brew install ngrok, or https://ngrok.com/download) and run this again. You don't need an ngrok account: your team provides the tunnel.`);if(!await Ka(t))throw Error(`nothing is running on 127.0.0.1:${t}. Start your app first, then share it.`);let a=new mt(e.api,()=>ar(e)),o=Bn(`Opening a link for 127.0.0.1:${t}`,{quiet:!0}),c;try{c=await a.openApp({name:r.name??``,port:t,access:r.public?`public`:`town`})}finally{o.stop()}let l=s(N(),`share`);y(l,{recursive:!0,mode:448});let u=s(l,`${c.app_id}.yml`);ae(u,`version: "3"
|
|
78
78
|
agent:
|
package/package.json
CHANGED