Repository navigation
Conversation
|
Thanks @code-yeongyu, really glad this is the fix you wanted! Whenever you get a chance to review, please share any feedback or changes you'd like and I'll update the PR quickly. Also happy to rebase if #11819 lands first. Looking forward to seeing it in a release. Thanks again! |
|
Thanks a lot for this, @meutex! The review found a few things to finish before it can land, and since it fixes an issue we filed (#11822), we're taking care of them ourselves as maintainer edits on top of your branch. Your commits and credit stay as they are. The main changes:
There's nothing you need to do. We'll post here once the update is pushed. Thanks again for picking this up! |
…ctor ownership by it (code-yeongyu#11863) The reflection lock recorded the raw platform machine id, which also spread into quarantine evidence. The id is now hashed at the source with a domain-separated sha256 (omo-reflection-host:) and every comparison uses the hash; locks written without one still fall back to hostname equality. The Senpi /doctor reservation check now uses classifyHostIdentity like reconcile and the omo-native doctor. Renames the changelog fragment to 11863.md.
# Conflicts: # packages/omo-senpi/plugin/extensions/memory-run-supervisor.mjs # packages/omo-senpi/plugin/extensions/omo-memory-commands.js # packages/omo-senpi/plugin/extensions/omo-memory-doctor.js # packages/omo-senpi/plugin/extensions/omo-memory-memfs.js # packages/omo-senpi/plugin/extensions/omo-memory-palace.js # packages/omo-senpi/plugin/extensions/omo-memory-sync.js # packages/omo-senpi/plugin/extensions/omo-onboarding-scan.js # packages/omo-senpi/plugin/extensions/omo-side-panel.js # packages/omo-senpi/plugin/extensions/omo.js
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| 39351725 | Triggered | Generic Password | 82eed01 | .github/workflows/ci.yml | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secret safely. Learn here the best practices.
- Revoke and rotate this secret.
- If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.
To avoid such incidents in the future consider
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.
There was a problem hiding this comment.
4 issues found across 29 files
Confidence score: 2/5
- The generated
memory-run-supervisor.mjsstill uses hostname-only stale-lock checks, so machines with the same hostname can reclaim each other’s live locks. Regenerate the bundle with the machine-ID rule. - In
host-identity.ts, matching machine hashes are treated as local even when hostnames differ, so cloned hosts sharing a memory directory can treat each other’s live locks as local. Keep a hostname mismatch from classifying the holder as local. - In
run-reconciliation.test.ts, the “never released or quarantined” test only checksactive.runId, so it can pass if reservation state changes. Compare the full state before and after reconciliation. - In
stale-holder.scenarios.test-support.ts, labels don’t map to// rule:tags andmachine-id-hot-pathis reused, so they can’t reliably identify which fail-closed rule a scenario covers. Align each label with a unique rule tag.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/omo-senpi/src/components/memory/worker/run-reconciliation.test.ts">
<violation number="1" location="packages/omo-senpi/src/components/memory/worker/run-reconciliation.test.ts:354">
P3: This test name promises the reservation is 'never released or quarantined', but it only checks that `active.runId` survives. Capture `await item.store.readState()` before reconciling and assert `toEqual` against it afterwards, matching the untouched-reservation test below. That also catches unintended rewrites of the launcher fields.</violation>
</file>
<file name="packages/memory-core/src/reflection/stale-holder.scenarios.test-support.ts">
<violation number="1" location="packages/memory-core/src/reflection/stale-holder.scenarios.test-support.ts:72">
P3: These new `rule` labels don't match any `// rule:` tag in `stale-holder.ts`, and `machine-id-hot-path` is reused by two scenarios. The file's own convention says each label names a fail-closed rule, so these labels break that contract. Use the production rule names, or drop the `rule` field for the non-rule cases.</violation>
</file>
<file name="packages/omo-senpi/plugin/extensions/memory-run-supervisor.mjs">
<violation number="1" location="packages/omo-senpi/plugin/extensions/memory-run-supervisor.mjs:2">
P1: This generated bundle does not contain the new machine-ID rule. The stale-lock check still compares only hostnames, so same-hostname machines can still reclaim each other's live locks. The hash reader is also emitted as an IIFE that never calls its async arrow, so it is dead code. Rebuild with the supported build path (`build-extension-core`) so the artifact matches `host-identity.ts` and `stale-holder.ts`.</violation>
</file>
<file name="packages/memory-core/src/reflection/host-identity.ts">
<violation number="1" location="packages/memory-core/src/reflection/host-identity.ts:84">
P2: Equal machine hashes return `local` even when the hostnames differ. Hosts that share a machine-id (cloned VM images, containers from one image) and a memory directory will now see each other's live locks as local, and the PID check runs against the wrong machine. Before this change the hostname mismatch made them `stuck`. Keep a hash match from overriding a hostname mismatch unless a same-host signal, such as the launcher process start identity, also matches.</violation>
</file>
Reply to a comment to ask cubic a question or push back. It learns from your replies.
View guided diff | Re-trigger cubic
| @@ -1,6 +1,6 @@ | |||
| #!/usr/bin/env node | |||
There was a problem hiding this comment.
P1: This generated bundle does not contain the new machine-ID rule. The stale-lock check still compares only hostnames, so same-hostname machines can still reclaim each other's live locks. The hash reader is also emitted as an IIFE that never calls its async arrow, so it is dead code. Rebuild with the supported build path (build-extension-core) so the artifact matches host-identity.ts and stale-holder.ts.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/omo-senpi/plugin/extensions/memory-run-supervisor.mjs, line 2:
<comment>This generated bundle does not contain the new machine-ID rule. The stale-lock check still compares only hostnames, so same-hostname machines can still reclaim each other's live locks. The hash reader is also emitted as an IIFE that never calls its async arrow, so it is dead code. Rebuild with the supported build path (`build-extension-core`) so the artifact matches `host-identity.ts` and `stale-holder.ts`.</comment>
<file context>
@@ -1,6 +1,6 @@
#!/usr/bin/env node
-// omo:17unDBtZ8IdzIBewpDPRQ6zCcIj8vuXhA57dubcbuQo:T3ymrGm5KDcZy6QTjJTp9Rcu2E3gGnhv758soXu6Vc4
-import{spawn as e}from"node:child_process";import{Buffer as t}from"node:buffer";import*as r from"node:fs";import*as n from"node:fs/promises";function i(e){if(e instanceof Error&&"code"in e)return"string"==typeof e.code?e.code:void 0}async function o(e){for(let t=0;;t+=1)try{return await e()}catch(e){if("EINTR"!==i(e)||t>=128)throw e}}function a(e){for(let t=0;;t+=1)try{return e()}catch(e){if("EINTR"!==i(e)||t>=128)throw e}}var s=new Set(["EPERM","EBUSY","EACCES"]),l=[10,25,50,100,200,400],c=e=>new Promise(t=>setTimeout(t,e));import{Buffer as u}from"node:buffer";import{constants as d}from"node:fs";import*as f from"node:fs/promises";function p(e){return"number"==typeof e?0!==(e&d.O_EXCL):"string"==typeof e&&e.toLowerCase().includes("x")}async function m(e,t,r,n){let i="string"==typeof t?u.from(t,r??"utf8"):t,a=0;for(;a<i.length;){n?.throwIfAborted();let{bytesWritten:t}=await o(()=>e.write(i,a));if(t<=0)throw Error(`fs write made no progress at offset ${a}`);a+=t}}function w(e){return"function"==typeof e}function h(e){return(...t)=>o(async()=>e(...t))}function y(e){return(...t)=>a(()=>e(...t))}function v(e,t){let r="async"===t?h:y,n=new Map;return new Proxy(e,{get(e,t){if(n.has(t))return n.get(t);let i=Reflect.get(e,t);if(!w(i))return i;let o=r(i),a=Reflect.get(i,"native");return w(a)&&Object.assign(o,{native:r(a)}),n.set(t,o),o}})}var g=new Set(["stat","sync","datasync","chmod","chown","utimes","truncate"]);var b=v(n,"async");function E(e,t,r){return async function(e,t,r){let n="win32"===r.platform?r.delaysMs??l:[],a=r.sleep??c;for(let l=0;;l+=1)try{return void await o(()=>void 0===r.admit?r.rename(e,t):r.admit(()=>r.rename(e,t)))}catch(e){let t=n[l],r=i(e);if(void 0===t||void 0===r||!s.has(r))throw e;await a(t)}}(e,t,{rename:(e,t)=>n.rename(e,t),platform:process.platform,...void 0===r?{}:{admit:r}})}var S=v(r,"sync"),{access:_,chmod:k,copyFile:x,cp:O,link:I,lstat:N,mkdir:$,mkdtemp:A,readFile:M,readdir:T,readlink:R,realpath:P,rm:C,rmdir:D,stat:L,statfs:j,unlink:z}=b,{accessSync:F,chmodSync:q,fsyncSync:U,linkSync:Z,lstatSync:B,mkdirSync:J,readFileSync:G,readdirSync:W,realpathSync:H,statSync:K,unlinkSync:Y,writeFileSync:V,writeSync:X}=S,{constants:Q,watch:ee}=r,te=async(...e)=>{let t=()=>n.open(...e);return function(e){return new Proxy(e,{get(e,t){let r=Reflect.get(e,t);if(!w(r))return r;let n=t=>Reflect.apply(r,e,[...t]);return"close"===t||t===Symbol.asyncDispose?(...e)=>{let t=n(e);return t instanceof Promise?t.catch(e=>{if("EINTR"!==i(e))throw e}):t}:"string"==typeof t&&g.has(t)?(...e)=>{let t=n(e);return t instanceof Promise?t.catch(t=>{if("EINTR"!==i(t))throw t;return o(async()=>n(e))}):t}:(...e)=>n(e)}})}(await(p(e[1]??"r")?t():o(t)))};var re=async(e,r,a)=>{if("string"!=typeof r&&!(r instanceof Uint8Array))return n.writeFile(e,r,a);if("object"==typeof(s=e)&&!(s instanceof URL)&&!t.isBuffer(s)&&"fd"in s){let t="string"==typeof a?{encoding:a}:a??{};return t.signal?.throwIfAborted(),await m(e,r,t.encoding??void 0,t.signal),void(!0===t.flush&&await o(()=>e.sync()))}var s;return async function(e,t,r,n){let a="string"==typeof r?{encoding:r}:r??{};a.signal?.throwIfAborted();let s=a.flag??n,l=()=>f.open(e,s,a.mode),c=await(p(s)?l():o(l));try{await m(c,t,a.encoding??"utf8",a.signal),!0===a.flush&&await o(()=>c.sync())}finally{await c.close().catch(e=>{if("EINTR"!==i(e))throw e})}}(e,r,a??void 0,"w")},ne=(e,t,n)=>function(e,t,r,n){return p(r)?e(t,r,n):a(()=>e(t,r,n))}(r.openSync,e,t,n??void 0);function ie(e){try{r.closeSync(e)}catch(e){if("EINTR"!==i(e))throw e}}function oe(e){try{return a(()=>r.statSync(e)),!0}catch{return!1}}import{join as ae}from"node:path";import{fileURLToPath as se}from"node:url";import{homedir as le}from"node:os";import{join as ce,resolve as ue}from"node:path";function de(e,t){let r=e.OMO_MEMORY_HOME;return void 0===r||""===r.trim()?ce(le(),".omo","memory"):ue(t,r)}import{constants as fe}from"node:os";var pe={darwin:{names:["/usr/lib/libSystem.B.dylib"],errnoSymbol:"__error"},linux:{names:["libc.so.6","libc.musl-x86_64.so.1","libc.musl-aarch64.so.1","libc.so"],errnoSymbol:"__errno_location"}},me=null;function we(){return me??=async function(){let e,t=pe[process.platform];if(void 0===t)return null;try{e=await(import("bun:ffi"))}catch{return null}for(let r of t.names)try{let n=e.dlopen(r,{flock:{args:[e.FFIType.i32,e.FFIType.i32],returns:e.FFIType.i32},[t.errnoSymbol]:{args:[],returns:e.FFIType.ptr}}),i=n.symbols[t.errnoSymbol];if(void 0===i)continue;return(t,r)=>{let o=Number(n.symbols.flock(t,r));if(0===o)return{rc:o,errno:0};let a=i();return{rc:o,errno:null===a?-1:e.read.i32(a,0)}}}catch{continue}return null}(),me}async function he(){return null!==await we()}fe.errno;import ye from"node:path";import ve from"node:path";import{randomUUID as ge}from"node:crypto";import{hostname as be}from"node:os";import{execFile as Ee}from"node:child_process";var Se=null;function _e(){return Se??=async function(){if("darwin"!==process.platform)return null;try{let{dlopen:e,FFIType:t}=await(import("bun:ffi")),r=e("/usr/lib/libSystem.B.dylib",{proc_pidinfo:{args:[t.i32,t.i32,t.u64,t.ptr,t.i32],returns:t.i32}});return(e,t,n,i,o)=>Number(r.symbols.proc_pidinfo(e,t,n,i,o))}catch{return null}}(),Se}var ke=null;async function xe(e){if("win32"!==process.platform)return null;if(!Number.isSafeInteger(e)||e<=0)return null;ke??=async function(){if("win32"!==process.platform)return null;try{let{dlopen:e,FFIType:t}=await(import("bun:ffi"));return e("kernel32.dll",{OpenProcess:{args:[t.u32,t.i32,t.u32],returns:t.ptr},GetProcessTimes:{args:[t.ptr,t.ptr,t.ptr,t.ptr,t.ptr],returns:t.i32},CloseHandle:{args:[t.ptr],returns:t.i32}}).symbols}catch{return null}}();let t=await ke;if(null===t)return null;let r=t.OpenProcess(4096,0,e);if(null===r||0===r||0n===r)return null;try{let e=new BigUint64Array(1),n=t.GetProcessTimes(r,e,new BigUint64Array(1),new BigUint64Array(1),new BigUint64Array(1)),i=e[0]??0n;return 0===n||0n===i?null:i}finally{t.CloseHandle(r)}}async function Oe(e,t){return await new Promise(r=>{Ee(e,t,{encoding:"utf8",timeout:2e3,windowsHide:!0},(e,t)=>{if(null!==e)return void r(null);let n=t.trim();r(n.length>0?n:null)})})}var Ie=function(e,t){let r;return n=>n!==t?e(n):(r??=e(n).then(e=>(null===e&&(r=void 0),e),e=>{throw r=void 0,e}),r)}(async function(e){if("linux"===process.platform)return await async function(e){try{let t=await M(`/proc/${e}/stat`,"utf8"),r=t.lastIndexOf(")");if(r<0)return null;let n=t.slice(r+2).trim().split(/\s+/)[19];return void 0===n?null:`linux-proc-start-ticks:${n}`}catch{return null}}(e);if("darwin"===process.platform||"freebsd"===process.platform){if("dead"===$e(e))return null;let t=await async function(e){if("darwin"!==process.platform)return null;if(!Number.isSafeInteger(e)||e<=0)return null;let t=await _e();if(null===t)return null;let r=new Uint8Array(136),n=0;try{n=t(e,3,0n,r,136)}catch{return null}if(136!==n)return null;let i=new DataView(r.buffer,r.byteOffset,r.byteLength),o=Number(i.getBigUint64(120,!0));return o>0?o:null}(e);if(null!==t)return`proc-start-epoch:${t}`;let r=await Oe("/bin/ps",["-o","lstart=","-p",String(e)]);return null===r?null:`ps-lstart:${r.replace(/\s+/g," ")}`}return"win32"===process.platform?await async function(e){if("dead"===$e(e))return null;let t=await xe(e);if(null!==t)return`win32-creation-filetime:${t}`;let r=await Oe("powershell.exe",["-NoProfile","-Command",`(Get-Process -Id ${e} -ErrorAction SilentlyContinue).StartTime.ToUniversalTime().ToString('o')`]);return null===r?null:`win32-creation-date:${r}`}(e):null},process.pid);function Ne(e){let t=e.indexOf(":");return t<=0?null:e.slice(0,t)}function $e(e){try{return process.kill(e,0),"alive"}catch(e){let t=function(e){if(e instanceof Error&&"code"in e)return"string"==typeof e.code?e.code:void 0}(e);return"ESRCH"===t?"dead":"EPERM"===t?"alive":"unknown"}}function Ae(e){return"string"==typeof e&&e.length>0}function Me(e){let t;try{t=JSON.parse(e)}catch{return null}if("object"!=typeof t||null===t||Array.isArray(t))return null;let r=t;return!Number.isInteger(r.pid)||Number(r.pid)<=0?null:Ae(r.process_start)&&Ae(r.hostname)&&Ae(r.nonce)?!Ae(r.created_at)||Number.isNaN(Date.parse(r.created_at))?null:Ae(r.purpose)&&(void 0===r.run_id||Ae(r.run_id))?{pid:Number(r.pid),process_start:r.process_start,hostname:r.hostname,nonce:r.nonce,created_at:r.created_at,purpose:r.purpose,...void 0===r.run_id?{}:{run_id:r.run_id},...!0===r.git_activity_lock?{git_activity_lock:!0}:{}}:null:null}async function Te(e,t={}){if(0===e.length)throw Error("lock purpose must not be empty");let r=await Ie(process.pid);return{pid:process.pid,process_start:r??"unavailable",hostname:be(),nonce:ge(),created_at:(new Date).toISOString(),purpose:e,...void 0===t.runId?{}:{run_id:t.runId},...await he()?{git_activity_lock:!0}:{}}}var Re=new Map;function Pe(e,t,r){Re.set(e,{previous:t,takeoverNonce:r})}function Ce(e,t){Re.get(e)?.takeoverNonce===t&&Re.delete(e)}function De(e){if(e instanceof Error&&"code"in e)return"string"==typeof e.code?e.code:void 0}async function Le(e){try{let t=await M(e,"utf8"),r=await L(e,{bigint:!0});return{raw:t,record:Me(t),dev:r.dev,ino:r.ino,mtimeMs:Number(r.mtimeMs)}}catch(e){let t=De(e);if("ENOENT"===t)return null;if("\\"===ve.sep&&"EPERM"===t)return{raw:"",record:null,dev:0,ino:0,mtimeMs:Date.now()};throw e}}function je(e,t){return e.dev===t.dev&&e.ino===t.ino&&e.mtimeMs===t.mtimeMs&&e.raw===t.raw}async function ze(e,t){Ce(e,t.nonce);let r=await Le(e);if(null===r||r.record?.nonce!==t.nonce)return!1;try{return await z(e),!0}catch(e){if("ENOENT"===De(e))return!1;throw e}}import{randomUUID as Fe}from"node:crypto";import qe from"node:path";import Ue from"node:path";var Ze=new Set;function Be(e){Ze.add(e)}function Je(e){Ze.delete(e)}var Ge=/\.candidate-(?:(\d+)-)?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;function We(e){let t=Ge.exec(e)?.[1];return void 0!==t&&"dead"===$e(Number(t))}function He(e){if(e instanceof Error&&"code"in e)return"string"==typeof e.code?e.code:void 0}function Ke(e,t){try{e.onFailure?.(t)}catch{}}function Ye(e){if("win32"!==process.platform)return!1;let t=He(e);return"EBUSY"===t||"EPERM"===t||"EACCES"===t}var Ve=new Map;function Xe(e){Ve.delete(e)}var Qe=new Set(["EACCES","EPERM","ENOTSUP"]);function et(e){if(e instanceof Error&&"code"in e)return"string"==typeof e.code?e.code:void 0}function tt(e,t){if(void 0!==t)return t(e);if("win32"!==process.platform)return!1;let r=et(e);return"EBUSY"===r||"EPERM"===r||"EACCES"===r}var rt={};function nt(){return rt}async function it(e){for(let t=0;t<3;t+=1)try{return await(rt.unlink??z)(e),Je(e),!0}catch(r){if("ENOENT"===et(r))return Je(e),!0;if(!tt(r,rt.isSharingError))throw Be(e),Xe(qe.dirname(e)),r;if(t+1===3)return Be(e),Xe(qe.dirname(e)),!1}return!1}async function ot(e,t){await $(qe.dirname(e),{recursive:!0,mode:448});let{candidatePath:r,handle:n}=await async function(e){for(let t=0;;t+=1){let r=`${e}.candidate-${process.pid}-${Fe()}`;try{return{candidatePath:r,handle:await te(r,"wx",384)}}catch(n){if(await it(r)||Xe(qe.dirname(e)),"EINTR"!==et(n)||t>=128)throw n}}}(e);try{try{await m(n,`${JSON.stringify(t)}\n`,"utf8"),await n.sync()}finally{await n.close()}try{return await(rt.link??I)(r,e),!0}catch(r){if(function(e){let t=et(e);return"EEXIST"===t||"ENOENT"===t}(r))return!1;if(Qe.has(et(r)??""))return await async function(e,t){let r;try{r=await te(e,"wx",384)}catch(e){if("EEXIST"===et(e))return!1;throw e}try{return await(rt.writeFallback??m)(r,`${JSON.stringify(t)}\n`,"utf8"),await r.sync(),!0}finally{await r.close()}}(e,t);throw r}}finally{await it(r)||Xe(qe.dirname(e))}}function at(e,t,r=Math.random){let n=Math.min(250,t*2**Math.min(e,30));return Math.max(1,Math.round(n/2+r()*n/2))}function st(e,t){return t?.throwIfAborted(),new Promise((r,n)=>{let i=setTimeout(a,e),o=()=>a(t?.reason??new DOMException("The operation was aborted","AbortError"));function a(e){clearTimeout(i),t?.removeEventListener("abort",o),void 0===e?r():n(e)}t?.addEventListener("abort",o,{once:!0})})}import{randomUUID as lt}from"node:crypto";import{hostname as ct}from"node:os";function ut(e){if(e instanceof Error&&"code"in e)return"string"==typeof e.code?e.code:void 0}async function dt(e){if(e.hostname!==ct())return!1;let t=$e(e.pid);if("dead"===t)return!0;if("unknown"===t)return!1;let r=await Ie(e.pid);return null!==r&&"unavailable"!==e.process_start&&function(e,t){let r=Ne(e);return null!==r&&r===Ne(t)&&e!==t}(e.process_start,r)}async function ft(e,t,r){return null!==e.record?dt(e.record):t-e.mtimeMs>r}async function pt(e,t,r){let n=await Le(e);if(null===n||!await ft(n,t,r))return!1;let i=`${e}.reaping-${lt()}`;try{await E(e,i)}catch(e){if("ENOENT"===ut(e))return!1;if(tt(e))return!1;throw e}let o=await Le(i);if(null!==o&&je(o,n))return await z(i),!0;try{await I(i,e)}catch(e){if("EEXIST"!==ut(e))throw e}return await z(i),!1}async function mt(e,t,r,n,i){if(!await ft(t,n,i))return!1;let o=`${e}.recovery`,a={...r,nonce:lt(),created_at:(new Date).toISOString(),purpose:`${r.purpose}:recovery`};for(let e=0;null!==await Le(o)||!await ot(o,a);e+=1)if(e>0||!await pt(o,n,i))return!1;try{let s=await Le(e);if(null===s)return!0;if(!je(s,t))return!1;if(!await ft(s,n,i))return!1;let l=await Le(o);return null!==l&&l.record?.nonce===a.nonce&&(await z(e),null!==s.record&&Pe(e,s.record,r.nonce),!0)}finally{await ze(o,a)}}class wt extends Error{lockPath;owner;retriable=!0;constructor(e,t){super(`Lock is held: ${e}`),this.lockPath=e,this.owner=t,this.name="LockContentionError"}}async function ht(e,t,r={}){let n=r.waitTimeoutMs??0,i=r.retryDelayMs??25,o=r.incompleteLockGraceMs??5e3,a=r.now??Date.now;if(n<0||i<=0||o<0)throw Error("lock wait options must be positive");let s=a()+n;try{return await async function(e,t,r,n){let{retryDelayMs:i,incompleteLockGraceMs:o,now:a,deadline:s}=n,l=!1,c=!1;for(let n=0;;n+=1){r.signal?.throwIfAborted();let u=await Le(e);if(null===u){if(await ot(e,t))return{sawOtherOwner:l};if(r.signal?.throwIfAborted(),l=!0,c=yt(e,t,c),u=await Le(e),null===u)continue}if(l=!0,c=yt(e,t,c),await mt(e,u,t,a(),o))c=!0;else{if(r.signal?.throwIfAborted(),a()>=s)throw new wt(e,u.record);await st(Math.min(at(n,i),Math.max(1,s-a())),r.signal)}}}(e,t,r,{retryDelayMs:i,incompleteLockGraceMs:o,now:a,deadline:s})}catch(r){throw Ce(e,t.nonce),r}}function yt(e,t,r){return r&&Ce(e,t.nonce),!1}import{hostname as vt}from"node:os";import gt from"node:path";function bt(e){if(e instanceof Error&&"code"in e)return"string"==typeof e.code?e.code:void 0}async function Et(e,t){let r=!0===(await N(e).catch(()=>{}))?.isFile()?await async function(e){let t;try{t=await M(e,"utf8")}catch(e){if("ENOENT"===bt(e))return null;throw e}let r=Me(t);return null===r||r.hostname!==vt()||"dead"!==$e(r.pid)?null:await dt(r)?r:null}(e):null;if(null===r)return!1;let n=await Te("dead-owner-sweep");try{await ht(e,n,{waitTimeoutMs:0})}catch(e){if(e instanceof wt)return!1;throw e}return await ze(e,n),t.onReclaimed?.(e,r),!0}async function St(e,t,r={}){let n,i=ye.dirname(e);if(function(e,t){let r=Ve.get(e);return!(void 0!==r&&t-r<6e5||(Ve.set(e,t),0))}(i,(r.now??Date.now)())){let t=nt();await async function(e,t=Date.now,r={}){let n;try{n=await T(e)}catch(t){if("ENOENT"===He(t)){for(let t of Ze)Ue.dirname(t)===e&&Je(t);return 0}throw t}let i=0;for(let o of n){if(!Ge.test(o))continue;let n=Ue.join(e,o);try{let e=await L(n);if(!(Ze.has(n)||We(o)||t()-e.mtimeMs>36e5))continue;let a=!1;for(let e=0;e<3;e+=1)try{await(r.unlink??z)(n),Je(n),a=!0;break}catch(t){if("ENOENT"===He(t)){Je(n),a=!0;break}if(!(r.isSharingError?.(t)??Ye(t)))throw t;e+1===3&&Ke(r,n)}a&&(i+=1)}catch(e){"ENOENT"===He(e)&&Je(n),Ke(r,n)}}return i}(i,Date.now,{...void 0===t.unlink?{}:{unlink:t.unlink},...void 0===t.isSharingError?{}:{isSharingError:t.isSharingError},onFailure:()=>Xe(i)}).catch(()=>{Xe(i)}),await async function(e,t={}){let r;try{r=await T(e)}catch(r){return"ENOENT"!==bt(r)&&t.onFailure?.(e,r),0}let n=0;for(let i of r){if(!i.endsWith(".lock"))continue;let r=gt.join(e,i);try{await Et(r,t)&&(n+=1)}catch(e){t.onFailure?.(r,e)}}return n}(i,{onReclaimed:(t,r)=>{ye.resolve(t)===ye.resolve(e)&&(n=r)}})}let o=await ht(e,t,r);void 0===n||o.sawOtherOwner||void 0!==function(e,t){let r=Re.get(e);return r?.takeoverNonce===t?r.previous:void 0}(e,t.nonce)||Pe(e,n,t.nonce)}async function _t(e,t,r,n){await St(e,t,n);try{return await r()}finally{await ze(e,t)}}import{randomUUID as kt}from"node:crypto";import xt from"node:path";var Ot=".ticket";function It(e,t,r){if(!Number.isInteger(r)||r<1)throw Error(`${t} slot must be a positive integer, got ${r}`);return xt.join(e,`${t}.slot-${r}.lock`)}var Nt=0,$t=Promise.resolve(),At={};async function Mt(e,t,r){await $(e,{recursive:!0,mode:448});let n=xt.join(e,`${t}.staging`);await re(n,`${JSON.stringify(r)}\n`,{mode:384}),await E(n,xt.join(e,t))}async function Tt(e){try{return(await T(e)).filter(e=>e.endsWith(Ot)).sort()}catch(e){if("ENOENT"===Lt(e))return[];throw e}}async function Rt(e){try{await z(e)}catch(e){if("ENOENT"!==Lt(e))throw e}}async function Pt(e,t){let r,n=xt.join(e,t);try{r=await(At.readFile??M)(n,"utf8")}catch(e){if("ENOENT"===Lt(e))return!0;if(function(e){if(void 0!==At.isSharingError)return At.isSharingError(e);if("win32"!==process.platform)return!1;let t=Lt(e);return"EBUSY"===t||"EPERM"===t||"EACCES"===t}(e))return!1;throw e}let i=Me(r);return!(null===i||!await dt(i))&&(await Rt(n),!0)}async function Ct(e,t,r,n){for(let i=1;i<=r;i+=1){let r=It(e,t.purpose,i);try{return await St(r,t,{waitTimeoutMs:0,...void 0===n?{}:{signal:n}}),{slot:i,release:()=>ze(r,t),retainFor:async(e,n)=>{let i=`${r}.handoff-${t.nonce}`;try{await re(i,`${JSON.stringify({...t,pid:e,process_start:n??"unavailable",git_activity_lock:void 0})}\n`,{mode:384}),await E(i,r)}finally{await Rt(i)}},releaseSync:()=>{try{return Me(G(r,"utf8"))?.nonce===t.nonce&&(Y(r),!0)}catch(e){if("ENOENT"===Lt(e))return!1;throw e}}}}catch(e){if(e instanceof wt)continue;throw e}}}async function Dt(e,t,r={}){let n=r.maxConcurrent??t.defaultSlots,i=r.waitTimeoutMs??t.waitTimeoutMs,o=r.retryDelayMs??25;if(!Number.isInteger(n)||n<1)throw Error(`${t.purpose} maxConcurrent must be a positive integer, got ${n}`);if(i<0||o<=0)throw Error("lock wait options must be positive");let{signal:a}=r,s=function(e,t){return xt.join(e,`${t}.tickets`)}(e,t.purpose),l=(Nt=(Nt+1)%1e6,`${String(Date.now()).padStart(16,"0")}-${String(Nt).padStart(6,"0")}-${String(process.pid).padStart(10,"0")}-${kt()}${Ot}`),c=Date.now(),u=c+i,d=$t.then(async()=>{a?.throwIfAborted(),await Mt(s,l,await Te(`${t.purpose}:ticket`))});$t=d.catch(()=>{});try{await d;let r=await Te(t.purpose);for(let i=0;;i+=1){a?.throwIfAborted();let d=await Tt(s),f=d[0];if(void 0!==f&&d.includes(l)){if(f===l){let t=await Ct(e,r,n,a);if(void 0!==t){try{await Rt(xt.join(s,l))}catch(e){throw await t.release(),e}return t}}else if(await Pt(s,f))continue}else await Mt(s,l,await Te(`${t.purpose}:ticket`));let p=Date.now();if(p>=u)throw t.busyError(p-c,n);await st(Math.min(at(i,o),Math.max(1,u-p)),a)}}finally{await Rt(xt.join(s,l)).catch(()=>{})}}function Lt(e){if(e instanceof Error&&"code"in e)return"string"==typeof e.code?e.code:void 0}(function(){let e=process.env.OMO_TEST_GIT_LOCK_SETTLE_MS;if(void 0===e)return;let t=Number.parseInt(e,10);Number.isInteger(t)})(),["process\\.env\\.[A-Za-z_][A-Za-z0-9_]*","os\\.environ(?:\\.get\\(|\\[)?","env\\.[A-Za-z_][A-Za-z0-9_]*","\\$\\{[A-Za-z_][A-Za-z0-9_]*\\}","\\$[A-Za-z_][A-Za-z0-9_]*","\\{\\{[^}\\n]*\\}\\}","\\[(?:redacted|filtered)\\]","(?:redacted|filtered)","(?:bearer|changeme|placeholder|example|none|null|undefined|true|false)"].join("|"),String.raw`#!/bin/sh
+// omo:8k2FT4GovKvN7YShtCDwrqWS38f4tm2u0bSbwfGeqqk:viYy2JhSMiPCPpKvkbLvBGbfTtgudufwwHbyvDvnL4o
+import{spawn as e}from"node:child_process";import{Buffer as t}from"node:buffer";import*as r from"node:fs";import*as n from"node:fs/promises";function i(e){if(e instanceof Error&&"code"in e)return"string"==typeof e.code?e.code:void 0}async function o(e){for(let t=0;;t+=1)try{return await e()}catch(e){if("EINTR"!==i(e)||t>=128)throw e}}function a(e){for(let t=0;;t+=1)try{return e()}catch(e){if("EINTR"!==i(e)||t>=128)throw e}}var s=new Set(["EPERM","EBUSY","EACCES"]),l=[10,25,50,100,200,400],c=e=>new Promise(t=>setTimeout(t,e));import{Buffer as u}from"node:buffer";import{constants as d}from"node:fs";import*as f from"node:fs/promises";function p(e){return"number"==typeof e?0!==(e&d.O_EXCL):"string"==typeof e&&e.toLowerCase().includes("x")}async function m(e,t,r,n){let i="string"==typeof t?u.from(t,r??"utf8"):t,a=0;for(;a<i.length;){n?.throwIfAborted();let{bytesWritten:t}=await o(()=>e.write(i,a));if(t<=0)throw Error(`fs write made no progress at offset ${a}`);a+=t}}function w(e){return"function"==typeof e}function h(e){return(...t)=>o(async()=>e(...t))}function y(e){return(...t)=>a(()=>e(...t))}function v(e,t){let r="async"===t?h:y,n=new Map;return new Proxy(e,{get(e,t){if(n.has(t))return n.get(t);let i=Reflect.get(e,t);if(!w(i))return i;let o=r(i),a=Reflect.get(i,"native");return w(a)&&Object.assign(o,{native:r(a)}),n.set(t,o),o}})}var g=new Set(["stat","sync","datasync","chmod","chown","utimes","truncate"]);var b=v(n,"async");function E(e,t,r){return async function(e,t,r){let n="win32"===r.platform?r.delaysMs??l:[],a=r.sleep??c;for(let l=0;;l+=1)try{return void await o(()=>void 0===r.admit?r.rename(e,t):r.admit(()=>r.rename(e,t)))}catch(e){let t=n[l],r=i(e);if(void 0===t||void 0===r||!s.has(r))throw e;await a(t)}}(e,t,{rename:(e,t)=>n.rename(e,t),platform:process.platform,...void 0===r?{}:{admit:r}})}var S=v(r,"sync"),{access:_,chmod:x,copyFile:k,cp:O,link:I,lstat:N,mkdir:$,mkdtemp:M,readFile:A,readdir:T,readlink:R,realpath:P,rm:D,rmdir:C,stat:L,statfs:j,unlink:F}=b,{accessSync:z,chmodSync:U,fsyncSync:q,linkSync:Z,lstatSync:B,mkdirSync:J,readFileSync:G,readdirSync:H,realpathSync:W,statSync:K,unlinkSync:Y,writeFileSync:V,writeSync:X}=S,{constants:Q,watch:ee}=r,te=async(...e)=>{let t=()=>n.open(...e);return function(e){return new Proxy(e,{get(e,t){let r=Reflect.get(e,t);if(!w(r))return r;let n=t=>Reflect.apply(r,e,[...t]);return"close"===t||t===Symbol.asyncDispose?(...e)=>{let t=n(e);return t instanceof Promise?t.catch(e=>{if("EINTR"!==i(e))throw e}):t}:"string"==typeof t&&g.has(t)?(...e)=>{let t=n(e);return t instanceof Promise?t.catch(t=>{if("EINTR"!==i(t))throw t;return o(async()=>n(e))}):t}:(...e)=>n(e)}})}(await(p(e[1]??"r")?t():o(t)))};var re=async(e,r,a)=>{if("string"!=typeof r&&!(r instanceof Uint8Array))return n.writeFile(e,r,a);if("object"==typeof(s=e)&&!(s instanceof URL)&&!t.isBuffer(s)&&"fd"in s){let t="string"==typeof a?{encoding:a}:a??{};return t.signal?.throwIfAborted(),await m(e,r,t.encoding??void 0,t.signal),void(!0===t.flush&&await o(()=>e.sync()))}var s;return async function(e,t,r,n){let a="string"==typeof r?{encoding:r}:r??{};a.signal?.throwIfAborted();let s=a.flag??n,l=()=>f.open(e,s,a.mode),c=await(p(s)?l():o(l));try{await m(c,t,a.encoding??"utf8",a.signal),!0===a.flush&&await o(()=>c.sync())}finally{await c.close().catch(e=>{if("EINTR"!==i(e))throw e})}}(e,r,a??void 0,"w")},ne=(e,t,n)=>function(e,t,r,n){return p(r)?e(t,r,n):a(()=>e(t,r,n))}(r.openSync,e,t,n??void 0);function ie(e){try{r.closeSync(e)}catch(e){if("EINTR"!==i(e))throw e}}function oe(e){try{return a(()=>r.statSync(e)),!0}catch{return!1}}import{join as ae}from"node:path";import{fileURLToPath as se}from"node:url";import{homedir as le}from"node:os";import{join as ce,resolve as ue}from"node:path";function de(e,t){let r=e.OMO_MEMORY_HOME;return void 0===r||""===r.trim()?ce(le(),".omo","memory"):ue(t,r)}import{constants as fe}from"node:os";var pe={darwin:{names:["/usr/lib/libSystem.B.dylib"],errnoSymbol:"__error"},linux:{names:["libc.so.6","libc.musl-x86_64.so.1","libc.musl-aarch64.so.1","libc.so"],errnoSymbol:"__errno_location"}},me=null;function we(){return me??=async function(){let e,t=pe[process.platform];if(void 0===t)return null;try{e=await(import("bun:ffi"))}catch{return null}for(let r of t.names)try{let n=e.dlopen(r,{flock:{args:[e.FFIType.i32,e.FFIType.i32],returns:e.FFIType.i32},[t.errnoSymbol]:{args:[],returns:e.FFIType.ptr}}),i=n.symbols[t.errnoSymbol];if(void 0===i)continue;return(t,r)=>{let o=Number(n.symbols.flock(t,r));if(0===o)return{rc:o,errno:0};let a=i();return{rc:o,errno:null===a?-1:e.read.i32(a,0)}}}catch{continue}return null}(),me}async function he(){return null!==await we()}fe.errno;import ye from"node:path";import ve from"node:path";import{randomUUID as ge}from"node:crypto";import{hostname as be}from"node:os";import{execFile as Ee}from"node:child_process";var Se=null;function _e(){return Se??=async function(){if("darwin"!==process.platform)return null;try{let{dlopen:e,FFIType:t}=await(import("bun:ffi")),r=e("/usr/lib/libSystem.B.dylib",{proc_pidinfo:{args:[t.i32,t.i32,t.u64,t.ptr,t.i32],returns:t.i32}});return(e,t,n,i,o)=>Number(r.symbols.proc_pidinfo(e,t,n,i,o))}catch{return null}}(),Se}var xe=null;async function ke(e){if("win32"!==process.platform)return null;if(!Number.isSafeInteger(e)||e<=0)return null;xe??=async function(){if("win32"!==process.platform)return null;try{let{dlopen:e,FFIType:t}=await(import("bun:ffi"));return e("kernel32.dll",{OpenProcess:{args:[t.u32,t.i32,t.u32],returns:t.ptr},GetProcessTimes:{args:[t.ptr,t.ptr,t.ptr,t.ptr,t.ptr],returns:t.i32},CloseHandle:{args:[t.ptr],returns:t.i32}}).symbols}catch{return null}}();let t=await xe;if(null===t)return null;let r=t.OpenProcess(4096,0,e);if(null===r||0===r||0n===r)return null;try{let e=new BigUint64Array(1),n=t.GetProcessTimes(r,e,new BigUint64Array(1),new BigUint64Array(1),new BigUint64Array(1)),i=e[0]??0n;return 0===n||0n===i?null:i}finally{t.CloseHandle(r)}}async function Oe(e,t){return await new Promise(r=>{Ee(e,t,{encoding:"utf8",timeout:2e3,windowsHide:!0},(e,t)=>{if(null!==e)return void r(null);let n=t.trim();r(n.length>0?n:null)})})}var Ie=function(e,t){let r;return n=>n!==t?e(n):(r??=e(n).then(e=>(null===e&&(r=void 0),e),e=>{throw r=void 0,e}),r)}(async function(e){if("linux"===process.platform)return await async function(e){try{let t=await A(`/proc/${e}/stat`,"utf8"),r=t.lastIndexOf(")");if(r<0)return null;let n=t.slice(r+2).trim().split(/\s+/)[19];return void 0===n?null:`linux-proc-start-ticks:${n}`}catch{return null}}(e);if("darwin"===process.platform||"freebsd"===process.platform){if("dead"===$e(e))return null;let t=await async function(e){if("darwin"!==process.platform)return null;if(!Number.isSafeInteger(e)||e<=0)return null;let t=await _e();if(null===t)return null;let r=new Uint8Array(136),n=0;try{n=t(e,3,0n,r,136)}catch{return null}if(136!==n)return null;let i=new DataView(r.buffer,r.byteOffset,r.byteLength),o=Number(i.getBigUint64(120,!0));return o>0?o:null}(e);if(null!==t)return`proc-start-epoch:${t}`;let r=await Oe("/bin/ps",["-o","lstart=","-p",String(e)]);return null===r?null:`ps-lstart:${r.replace(/\s+/g," ")}`}return"win32"===process.platform?await async function(e){if("dead"===$e(e))return null;let t=await ke(e);if(null!==t)return`win32-creation-filetime:${t}`;let r=await Oe("powershell.exe",["-NoProfile","-Command",`(Get-Process -Id ${e} -ErrorAction SilentlyContinue).StartTime.ToUniversalTime().ToString('o')`]);return null===r?null:`win32-creation-date:${r}`}(e):null},process.pid);function Ne(e){let t=e.indexOf(":");return t<=0?null:e.slice(0,t)}function $e(e){try{return process.kill(e,0),"alive"}catch(e){let t=function(e){if(e instanceof Error&&"code"in e)return"string"==typeof e.code?e.code:void 0}(e);return"ESRCH"===t?"dead":"EPERM"===t?"alive":"unknown"}}function Me(e){return"string"==typeof e&&e.length>0}function Ae(e){let t;try{t=JSON.parse(e)}catch{return null}if("object"!=typeof t||null===t||Array.isArray(t))return null;let r=t;return!Number.isInteger(r.pid)||Number(r.pid)<=0?null:Me(r.process_start)&&Me(r.hostname)&&Me(r.nonce)?!Me(r.created_at)||Number.isNaN(Date.parse(r.created_at))?null:Me(r.purpose)&&(void 0===r.run_id||Me(r.run_id))?{pid:Number(r.pid),process_start:r.process_start,hostname:r.hostname,nonce:r.nonce,created_at:r.created_at,purpose:r.purpose,...void 0===r.run_id?{}:{run_id:r.run_id},...!0===r.git_activity_lock?{git_activity_lock:!0}:{}}:null:null}async function Te(e,t={}){if(0===e.length)throw Error("lock purpose must not be empty");let r=await Ie(process.pid);return{pid:process.pid,process_start:r??"unavailable",hostname:be(),nonce:ge(),created_at:(new Date).toISOString(),purpose:e,...void 0===t.runId?{}:{run_id:t.runId},...await he()?{git_activity_lock:!0}:{}}}var Re=new Map;function Pe(e,t,r){Re.set(e,{previous:t,takeoverNonce:r})}function De(e,t){Re.get(e)?.takeoverNonce===t&&Re.delete(e)}function Ce(e){if(e instanceof Error&&"code"in e)return"string"==typeof e.code?e.code:void 0}async function Le(e){try{let t=await A(e,"utf8"),r=await L(e,{bigint:!0});return{raw:t,record:Ae(t),dev:r.dev,ino:r.ino,mtimeMs:Number(r.mtimeMs)}}catch(e){let t=Ce(e);if("ENOENT"===t)return null;if("\\"===ve.sep&&"EPERM"===t)return{raw:"",record:null,dev:0,ino:0,mtimeMs:Date.now()};throw e}}function je(e,t){return e.dev===t.dev&&e.ino===t.ino&&e.mtimeMs===t.mtimeMs&&e.raw===t.raw}async function Fe(e,t){De(e,t.nonce);let r=await Le(e);if(null===r||r.record?.nonce!==t.nonce)return!1;try{return await F(e),!0}catch(e){if("ENOENT"===Ce(e))return!1;throw e}}import{randomUUID as ze}from"node:crypto";import Ue from"node:path";import qe from"node:path";var Ze=new Set;function Be(e){Ze.add(e)}function Je(e){Ze.delete(e)}var Ge=/\.candidate-(?:(\d+)-)?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;function He(e){let t=Ge.exec(e)?.[1];return void 0!==t&&"dead"===$e(Number(t))}function We(e){if(e instanceof Error&&"code"in e)return"string"==typeof e.code?e.code:void 0}function Ke(e,t){try{e.onFailure?.(t)}catch{}}function Ye(e){if("win32"!==process.platform)return!1;let t=We(e);return"EBUSY"===t||"EPERM"===t||"EACCES"===t}var Ve=new Map;function Xe(e){Ve.delete(e)}var Qe=new Set(["EACCES","EPERM","ENOTSUP"]);function et(e){if(e instanceof Error&&"code"in e)return"string"==typeof e.code?e.code:void 0}function tt(e,t){if(void 0!==t)return t(e);if("win32"!==process.platform)return!1;let r=et(e);return"EBUSY"===r||"EPERM"===r||"EACCES"===r}var rt={};function nt(){return rt}async function it(e){for(let t=0;t<3;t+=1)try{return await(rt.unlink??F)(e),Je(e),!0}catch(r){if("ENOENT"===et(r))return Je(e),!0;if(!tt(r,rt.isSharingError))throw Be(e),Xe(Ue.dirname(e)),r;if(t+1===3)return Be(e),Xe(Ue.dirname(e)),!1}return!1}async function ot(e,t){await $(Ue.dirname(e),{recursive:!0,mode:448});let{candidatePath:r,handle:n}=await async function(e){for(let t=0;;t+=1){let r=`${e}.candidate-${process.pid}-${ze()}`;try{return{candidatePath:r,handle:await te(r,"wx",384)}}catch(n){if(await it(r)||Xe(Ue.dirname(e)),"EINTR"!==et(n)||t>=128)throw n}}}(e);try{try{await m(n,`${JSON.stringify(t)}\n`,"utf8"),await n.sync()}finally{await n.close()}try{return await(rt.link??I)(r,e),!0}catch(r){if(function(e){let t=et(e);return"EEXIST"===t||"ENOENT"===t}(r))return!1;if(Qe.has(et(r)??""))return await async function(e,t){let r;try{r=await te(e,"wx",384)}catch(e){if("EEXIST"===et(e))return!1;throw e}try{return await(rt.writeFallback??m)(r,`${JSON.stringify(t)}\n`,"utf8"),await r.sync(),!0}finally{await r.close()}}(e,t);throw r}}finally{await it(r)||Xe(Ue.dirname(e))}}function at(e,t,r=Math.random){let n=Math.min(250,t*2**Math.min(e,30));return Math.max(1,Math.round(n/2+r()*n/2))}function st(e,t){return t?.throwIfAborted(),new Promise((r,n)=>{let i=setTimeout(a,e),o=()=>a(t?.reason??new DOMException("The operation was aborted","AbortError"));function a(e){clearTimeout(i),t?.removeEventListener("abort",o),void 0===e?r():n(e)}t?.addEventListener("abort",o,{once:!0})})}import{randomUUID as lt}from"node:crypto";import{hostname as ct}from"node:os";function ut(e){if(e instanceof Error&&"code"in e)return"string"==typeof e.code?e.code:void 0}async function dt(e){if(e.hostname!==ct())return!1;let t=$e(e.pid);if("dead"===t)return!0;if("unknown"===t)return!1;let r=await Ie(e.pid);return null!==r&&"unavailable"!==e.process_start&&function(e,t){let r=Ne(e);return null!==r&&r===Ne(t)&&e!==t}(e.process_start,r)}async function ft(e,t,r){return null!==e.record?dt(e.record):t-e.mtimeMs>r}async function pt(e,t,r){let n=await Le(e);if(null===n||!await ft(n,t,r))return!1;let i=`${e}.reaping-${lt()}`;try{await E(e,i)}catch(e){if("ENOENT"===ut(e))return!1;if(tt(e))return!1;throw e}let o=await Le(i);if(null!==o&&je(o,n))return await F(i),!0;try{await I(i,e)}catch(e){if("EEXIST"!==ut(e))throw e}return await F(i),!1}async function mt(e,t,r,n,i){if(!await ft(t,n,i))return!1;let o=`${e}.recovery`,a={...r,nonce:lt(),created_at:(new Date).toISOString(),purpose:`${r.purpose}:recovery`};for(let e=0;null!==await Le(o)||!await ot(o,a);e+=1)if(e>0||!await pt(o,n,i))return!1;try{let s=await Le(e);if(null===s)return!0;if(!je(s,t))return!1;if(!await ft(s,n,i))return!1;let l=await Le(o);return null!==l&&l.record?.nonce===a.nonce&&(await F(e),null!==s.record&&Pe(e,s.record,r.nonce),!0)}finally{await Fe(o,a)}}class wt extends Error{lockPath;owner;retriable=!0;constructor(e,t){super(`Lock is held: ${e}`),this.lockPath=e,this.owner=t,this.name="LockContentionError"}}async function ht(e,t,r={}){let n=r.waitTimeoutMs??0,i=r.retryDelayMs??25,o=r.incompleteLockGraceMs??5e3,a=r.now??Date.now;if(n<0||i<=0||o<0)throw Error("lock wait options must be positive");let s=a()+n;try{return await async function(e,t,r,n){let{retryDelayMs:i,incompleteLockGraceMs:o,now:a,deadline:s}=n,l=!1,c=!1;for(let n=0;;n+=1){r.signal?.throwIfAborted();let u=await Le(e);if(null===u){if(await ot(e,t))return{sawOtherOwner:l};if(r.signal?.throwIfAborted(),l=!0,c=yt(e,t,c),u=await Le(e),null===u)continue}if(l=!0,c=yt(e,t,c),await mt(e,u,t,a(),o))c=!0;else{if(r.signal?.throwIfAborted(),a()>=s)throw new wt(e,u.record);await st(Math.min(at(n,i),Math.max(1,s-a())),r.signal)}}}(e,t,r,{retryDelayMs:i,incompleteLockGraceMs:o,now:a,deadline:s})}catch(r){throw De(e,t.nonce),r}}function yt(e,t,r){return r&&De(e,t.nonce),!1}import{hostname as vt}from"node:os";import gt from"node:path";function bt(e){if(e instanceof Error&&"code"in e)return"string"==typeof e.code?e.code:void 0}async function Et(e,t){let r=!0===(await N(e).catch(()=>{}))?.isFile()?await async function(e){let t;try{t=await A(e,"utf8")}catch(e){if("ENOENT"===bt(e))return null;throw e}let r=Ae(t);return null===r||r.hostname!==vt()||"dead"!==$e(r.pid)?null:await dt(r)?r:null}(e):null;if(null===r)return!1;let n=await Te("dead-owner-sweep");try{await ht(e,n,{waitTimeoutMs:0})}catch(e){if(e instanceof wt)return!1;throw e}return await Fe(e,n),t.onReclaimed?.(e,r),!0}async function St(e,t,r={}){let n,i=ye.dirname(e);if(function(e,t){let r=Ve.get(e);return!(void 0!==r&&t-r<6e5||(Ve.set(e,t),0))}(i,(r.now??Date.now)())){let t=nt();await async function(e,t=Date.now,r={}){let n;try{n=await T(e)}catch(t){if("ENOENT"===We(t)){for(let t of Ze)qe.dirname(t)===e&&Je(t);return 0}throw t}let i=0;for(let o of n){if(!Ge.test(o))continue;let n=qe.join(e,o);try{let e=await L(n);if(!(Ze.has(n)||He(o)||t()-e.mtimeMs>36e5))continue;let a=!1;for(let e=0;e<3;e+=1)try{await(r.unlink??F)(n),Je(n),a=!0;break}catch(t){if("ENOENT"===We(t)){Je(n),a=!0;break}if(!(r.isSharingError?.(t)??Ye(t)))throw t;e+1===3&&Ke(r,n)}a&&(i+=1)}catch(e){"ENOENT"===We(e)&&Je(n),Ke(r,n)}}return i}(i,Date.now,{...void 0===t.unlink?{}:{unlink:t.unlink},...void 0===t.isSharingError?{}:{isSharingError:t.isSharingError},onFailure:()=>Xe(i)}).catch(()=>{Xe(i)}),await async function(e,t={}){let r;try{r=await T(e)}catch(r){return"ENOENT"!==bt(r)&&t.onFailure?.(e,r),0}let n=0;for(let i of r){if(!i.endsWith(".lock"))continue;let r=gt.join(e,i);try{await Et(r,t)&&(n+=1)}catch(e){t.onFailure?.(r,e)}}return n}(i,{onReclaimed:(t,r)=>{ye.resolve(t)===ye.resolve(e)&&(n=r)}})}let o=await ht(e,t,r);void 0===n||o.sawOtherOwner||void 0!==function(e,t){let r=Re.get(e);return r?.takeoverNonce===t?r.previous:void 0}(e,t.nonce)||Pe(e,n,t.nonce)}async function _t(e,t,r,n){await St(e,t,n);try{return await r()}finally{await Fe(e,t)}}import{randomUUID as xt}from"node:crypto";import kt from"node:path";var Ot=".ticket";function It(e,t,r){if(!Number.isInteger(r)||r<1)throw Error(`${t} slot must be a positive integer, got ${r}`);return kt.join(e,`${t}.slot-${r}.lock`)}var Nt=0,$t=Promise.resolve(),Mt={};async function At(e,t,r){await $(e,{recursive:!0,mode:448});let n=kt.join(e,`${t}.staging`);await re(n,`${JSON.stringify(r)}\n`,{mode:384}),await E(n,kt.join(e,t))}async function Tt(e){try{return(await T(e)).filter(e=>e.endsWith(Ot)).sort()}catch(e){if("ENOENT"===Lt(e))return[];throw e}}async function Rt(e){try{await F(e)}catch(e){if("ENOENT"!==Lt(e))throw e}}async function Pt(e,t){let r,n=kt.join(e,t);try{r=await(Mt.readFile??A)(n,"utf8")}catch(e){if("ENOENT"===Lt(e))return!0;if(function(e){if(void 0!==Mt.isSharingError)return Mt.isSharingError(e);if("win32"!==process.platform)return!1;let t=Lt(e);return"EBUSY"===t||"EPERM"===t||"EACCES"===t}(e))return!1;throw e}let i=Ae(r);return!(null===i||!await dt(i))&&(await Rt(n),!0)}async function Dt(e,t,r,n){for(let i=1;i<=r;i+=1){let r=It(e,t.purpose,i);try{return await St(r,t,{waitTimeoutMs:0,...void 0===n?{}:{signal:n}}),{slot:i,release:()=>Fe(r,t),retainFor:async(e,n)=>{let i=`${r}.handoff-${t.nonce}`;try{await re(i,`${JSON.stringify({...t,pid:e,process_start:n??"unavailable",git_activity_lock:void 0})}\n`,{mode:384}),await E(i,r)}finally{await Rt(i)}},releaseSync:()=>{try{return Ae(G(r,"utf8"))?.nonce===t.nonce&&(Y(r),!0)}catch(e){if("ENOENT"===Lt(e))return!1;throw e}}}}catch(e){if(e instanceof wt)continue;throw e}}}async function Ct(e,t,r={}){let n=r.maxConcurrent??t.defaultSlots,i=r.waitTimeoutMs??t.waitTimeoutMs,o=r.retryDelayMs??25;if(!Number.isInteger(n)||n<1)throw Error(`${t.purpose} maxConcurrent must be a positive integer, got ${n}`);if(i<0||o<=0)throw Error("lock wait options must be positive");let{signal:a}=r,s=function(e,t){return kt.join(e,`${t}.tickets`)}(e,t.purpose),l=(Nt=(Nt+1)%1e6,`${String(Date.now()).padStart(16,"0")}-${String(Nt).padStart(6,"0")}-${String(process.pid).padStart(10,"0")}-${xt()}${Ot}`),c=Date.now(),u=c+i,d=$t.then(async()=>{a?.throwIfAborted(),await At(s,l,await Te(`${t.purpose}:ticket`))});$t=d.catch(()=>{});try{await d;let r=await Te(t.purpose);for(let i=0;;i+=1){a?.throwIfAborted();let d=await Tt(s),f=d[0];if(void 0!==f&&d.includes(l)){if(f===l){let t=await Dt(e,r,n,a);if(void 0!==t){try{await Rt(kt.join(s,l))}catch(e){throw await t.release(),e}return t}}else if(await Pt(s,f))continue}else await At(s,l,await Te(`${t.purpose}:ticket`));let p=Date.now();if(p>=u)throw t.busyError(p-c,n);await st(Math.min(at(i,o),Math.max(1,u-p)),a)}}finally{await Rt(kt.join(s,l)).catch(()=>{})}}function Lt(e){if(e instanceof Error&&"code"in e)return"string"==typeof e.code?e.code:void 0}(function(){let e=process.env.OMO_TEST_GIT_LOCK_SETTLE_MS;if(void 0===e)return;let t=Number.parseInt(e,10);Number.isInteger(t)})(),["process\\.env\\.[A-Za-z_][A-Za-z0-9_]*","os\\.environ(?:\\.get\\(|\\[)?","env\\.[A-Za-z_][A-Za-z0-9_]*","\\$\\{[A-Za-z_][A-Za-z0-9_]*\\}","\\$[A-Za-z_][A-Za-z0-9_]*","\\{\\{[^}\\n]*\\}\\}","\\[(?:redacted|filtered)\\]","(?:redacted|filtered)","(?:bearer|changeme|placeholder|example|none|null|undefined|true|false)"].join("|"),String.raw`#!/bin/sh
# Refuse a hand-written claim receipt header in memory notes. Installed by omo memory-core; regenerated on startup.
</file context>
| if (currentMachineHash === null) { | ||
| return hostnameVerdict(facts.recordedHostname, facts.currentHostname) | ||
| } | ||
| return facts.recordedMachineHash === currentMachineHash |
There was a problem hiding this comment.
P2: Equal machine hashes return local even when the hostnames differ. Hosts that share a machine-id (cloned VM images, containers from one image) and a memory directory will now see each other's live locks as local, and the PID check runs against the wrong machine. Before this change the hostname mismatch made them stuck. Keep a hash match from overriding a hostname mismatch unless a same-host signal, such as the launcher process start identity, also matches.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/memory-core/src/reflection/host-identity.ts, line 84:
<comment>Equal machine hashes return `local` even when the hostnames differ. Hosts that share a machine-id (cloned VM images, containers from one image) and a memory directory will now see each other's live locks as local, and the PID check runs against the wrong machine. Before this change the hostname mismatch made them `stuck`. Keep a hash match from overriding a hostname mismatch unless a same-host signal, such as the launcher process start identity, also matches.</comment>
<file context>
@@ -0,0 +1,93 @@
+ if (currentMachineHash === null) {
+ return hostnameVerdict(facts.recordedHostname, facts.currentHostname)
+ }
+ return facts.recordedMachineHash === currentMachineHash
+ ? { state: "local" }
+ : { state: "foreign", recordedHostname: facts.recordedHostname, viaMachineId: true }
</file context>
|
|
||
| // then | ||
| expect(results).toEqual([]) | ||
| expect((await item.store.readState()).active?.runId).toBe("run-orphan") |
There was a problem hiding this comment.
P3: This test name promises the reservation is 'never released or quarantined', but it only checks that active.runId survives. Capture await item.store.readState() before reconciling and assert toEqual against it afterwards, matching the untouched-reservation test below. That also catches unintended rewrites of the launcher fields.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/omo-senpi/src/components/memory/worker/run-reconciliation.test.ts, line 354:
<comment>This test name promises the reservation is 'never released or quarantined', but it only checks that `active.runId` survives. Capture `await item.store.readState()` before reconciling and assert `toEqual` against it afterwards, matching the untouched-reservation test below. That also catches unintended rewrites of the launcher fields.</comment>
<file context>
@@ -324,6 +333,48 @@ describe("reflection and dream run reconciliation", () => {
+
+ // then
+ expect(results).toEqual([])
+ expect((await item.store.readState()).active?.runId).toBe("run-orphan")
+ }, 30_000)
+
</file context>
| { name: "another host owns the lock", rule: "other-host", facts: deadHolder({ files: { [LOCK]: lock({ launcherHostname: "other-host" }) } }), expected: stuck(RUN, "it is owned by host other-host, which cannot be checked from here") }, | ||
| { name: "another machine holds the lock under the same hostname", rule: "machine-id", facts: deadHolder({ machineId: "machine-b", files: { [LOCK]: lock({ launcherMachineHash: "machine-a" }) } }), expected: stuck(RUN, "it is owned by a different machine (host this-host), which cannot be checked from here") }, | ||
| { name: "the same machine with a renamed hostname still owns its dead lock", rule: "machine-id-renamed-host", facts: deadHolder({ machineId: "machine-a", files: { [LOCK]: lock({ launcherMachineHash: "machine-a", launcherHostname: "other-host" }) } }), expected: { state: "dead", runId: RUN } }, | ||
| { name: "a null machine id lookup falls back to hostname equality", rule: "machine-id-null", facts: deadHolder({ machineId: null, files: { [LOCK]: lock({ launcherMachineHash: "machine-a" }) } }), expected: { state: "dead", runId: RUN } }, |
There was a problem hiding this comment.
P3: These new rule labels don't match any // rule: tag in stale-holder.ts, and machine-id-hot-path is reused by two scenarios. The file's own convention says each label names a fail-closed rule, so these labels break that contract. Use the production rule names, or drop the rule field for the non-rule cases.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At packages/memory-core/src/reflection/stale-holder.scenarios.test-support.ts, line 72:
<comment>These new `rule` labels don't match any `// rule:` tag in `stale-holder.ts`, and `machine-id-hot-path` is reused by two scenarios. The file's own convention says each label names a fail-closed rule, so these labels break that contract. Use the production rule names, or drop the `rule` field for the non-rule cases.</comment>
<file context>
@@ -53,6 +67,11 @@ export const SCENARIOS: readonly Scenario[] = [
{ name: "another host owns the lock", rule: "other-host", facts: deadHolder({ files: { [LOCK]: lock({ launcherHostname: "other-host" }) } }), expected: stuck(RUN, "it is owned by host other-host, which cannot be checked from here") },
+ { name: "another machine holds the lock under the same hostname", rule: "machine-id", facts: deadHolder({ machineId: "machine-b", files: { [LOCK]: lock({ launcherMachineHash: "machine-a" }) } }), expected: stuck(RUN, "it is owned by a different machine (host this-host), which cannot be checked from here") },
+ { name: "the same machine with a renamed hostname still owns its dead lock", rule: "machine-id-renamed-host", facts: deadHolder({ machineId: "machine-a", files: { [LOCK]: lock({ launcherMachineHash: "machine-a", launcherHostname: "other-host" }) } }), expected: { state: "dead", runId: RUN } },
+ { name: "a null machine id lookup falls back to hostname equality", rule: "machine-id-null", facts: deadHolder({ machineId: null, files: { [LOCK]: lock({ launcherMachineHash: "machine-a" }) } }), expected: { state: "dead", runId: RUN } },
+ { name: "no machine id is read when active.lock is absent", rule: "machine-id-hot-path", facts: deadHolder({ failMachineId: true, files: { [LOCK]: undefined } }), expected: { state: "none" } },
+ { name: "no machine id is read when the record carries no launcherMachineHash", rule: "machine-id-hot-path", facts: deadHolder({ failMachineId: true }), expected: { state: "dead", runId: RUN } },
</file context>
# Conflicts: # packages/omo-senpi/plugin/extensions/memory-run-supervisor.mjs # packages/omo-senpi/plugin/extensions/omo-memory-commands.js # packages/omo-senpi/plugin/extensions/omo-memory-doctor.js # packages/omo-senpi/plugin/extensions/omo-memory-memfs.js # packages/omo-senpi/plugin/extensions/omo-memory-palace.js # packages/omo-senpi/plugin/extensions/omo-memory-sync.js # packages/omo-senpi/plugin/extensions/omo-onboarding-scan.js # packages/omo-senpi/plugin/extensions/omo-side-panel.js # packages/omo-senpi/plugin/extensions/omo.js
# Conflicts: # packages/omo-senpi/plugin/extensions/memory-run-supervisor.mjs # packages/omo-senpi/plugin/extensions/omo-memory-commands.js # packages/omo-senpi/plugin/extensions/omo-memory-doctor.js # packages/omo-senpi/plugin/extensions/omo-memory-memfs.js # packages/omo-senpi/plugin/extensions/omo-memory-palace.js # packages/omo-senpi/plugin/extensions/omo-memory-sync.js # packages/omo-senpi/plugin/extensions/omo-onboarding-scan.js # packages/omo-senpi/plugin/extensions/omo-side-panel.js # packages/omo-senpi/plugin/extensions/omo.js
The machine-id reader and hashed host rule (code-yeongyu#11863) entered omo.js's eager graph and pushed it 2,030 B past bundle-budget.json, failing bundle-size and the OTLP small-main-entry byte check (one cause). They now load through memory-core's #memory-host-identity, mapped to the recall-store sidecar in the plugin manifest (the code-yeongyu#11994 / #memory-external-projection mechanism). Callers pass the holder record as-is; the rule owns the type guard, the missing-hostname case and the default reader. omo.js grows +478 B over dev instead (locally 1,460,004 B, budget 1,460,160). A build test pins the boundary.
…azy #memory-host-identity import alias) (code-yeongyu#11863)
# Conflicts: # packages/omo-senpi/plugin/extensions/memory-run-supervisor.mjs # packages/omo-senpi/plugin/extensions/omo-claim-receipt.js # packages/omo-senpi/plugin/extensions/omo-computer-use.js # packages/omo-senpi/plugin/extensions/omo-desktop-toolkit.js # packages/omo-senpi/plugin/extensions/omo-device-account.js # packages/omo-senpi/plugin/extensions/omo-feedback.js # packages/omo-senpi/plugin/extensions/omo-init-deep-advisor.js # packages/omo-senpi/plugin/extensions/omo-member.js # packages/omo-senpi/plugin/extensions/omo-memory-commands.js # packages/omo-senpi/plugin/extensions/omo-memory-doctor.js # packages/omo-senpi/plugin/extensions/omo-memory-memfs.js # packages/omo-senpi/plugin/extensions/omo-memory-palace.js # packages/omo-senpi/plugin/extensions/omo-memory-sync.js # packages/omo-senpi/plugin/extensions/omo-onboarding-scan.js # packages/omo-senpi/plugin/extensions/omo-recall-store.js # packages/omo-senpi/plugin/extensions/omo-session-history.js # packages/omo-senpi/plugin/extensions/omo-side-panel.js # packages/omo-senpi/plugin/extensions/omo-task.js # packages/omo-senpi/plugin/extensions/omo-telemetry-otlp.js # packages/omo-senpi/plugin/extensions/omo-telemetry-posthog.js # packages/omo-senpi/plugin/extensions/omo-telemetry-recovery.js # packages/omo-senpi/plugin/extensions/omo-thread-client.js # packages/omo-senpi/plugin/extensions/omo.js
Problem
As someone who runs OmO memory on more than one machine that share a memory directory, I want the reflection lock's holder identity to be unique per machine, so that another machine's live reflection run is never mistaken for my own dead run and reclaimed.
Change
The reflection lock records which machine holds it only by hostname, and hostnames are not globally unique, so two of a user's machines with the same name treat each other's live lock as local, fail to find the recorded pid locally, and reclaim a run that is still running. Record a platform machine id alongside the hostname and decide "is this lock mine" by id first, keeping exact hostname equality as the fallback for legacy records and for a machine that cannot read its id.
stuck, neverdead, whatever the two hostnames say, and the reason text names the recorded hostname without printing either machine id.stuck. Covered by a test: a new scenario in packages/memory-core/src/reflection/stale-holder.scenarios.test-support.ts carrying arule:tag, asserted by stale-holder.test.ts, so deleting the id rule changes that scenario's verdict.dead. New scenario with its ownrule:tag.bun script/build-reflection-lock.ts(never hand-edited) and script/build-reflection-lock.test.ts, packages/omo-native/test/doctor-reflection-lock.test.ts and packages/omo-native/test/doctor-reflection-lock-parity.test.ts pass.bun run typecheck,bun test,tsgo --noEmit -p packages/omo-senpi/tsconfig.json,bun run test:senpi, andbun run lint:ratchet.### Fixedsection and non-empty text (style of changes/unreleased/11392.md).dev, is written in English, and fills .github/pull_request_template.md including QA & Evidence with decisive sanitized excerpts and a sha256 per evidence file (the evidence files themselves stay local and are never committed).Testing
rm -rf .sentrux && git status --short | head -40
exit=0
Fixes #11822
Summary by cubic
Records a hashed platform machine id beside the hostname in the reflection lock so two machines sharing a hostname no longer reclaim each other's live reflection run. The raw id is hashed at the source with a domain-separated sha256 and never stored or compared, including in quarantine evidence. Host identity is decided by the hash first, with exact hostname equality as the fallback for legacy records and unreadable ids.
stuck, neverdead, regardless of matching hostnames; a same-hash, different-hostname lock is still reclaimed.omo doctor, and the Senpi/doctorreservation check share the same rule through one exported module, loaded from the#memory-host-identitylazy sidecar soomo.jsstays under its bundle budget.Written for commit 89783c7. Summary will update on new commits.