beeos-claw 0.5.62 → 0.5.63
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.
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SESSION_RECOVERY_REQUIRED_CODE="session_recovery_required";export const SESSION_RECOVERY_REQUIRED_MESSAGE="The session could not be proven safe for another write. Recovery is required.";const DEFAULT_OBSERVATION_TIMEOUT_MS=13e4,DEFAULT_CANCELLATION_TERMINAL_OBSERVATION_TIMEOUT_MS=3e4;export class SessionMutationCoordinator{gate;quiescence;logger;states=new Map;observationTimeoutMs;cancellationTerminalObservationTimeoutMs;setTimer;clearTimer;onRecoveryRequired;constructor(e,r,o,t={}){this.gate=e,this.quiescence=r,this.logger=o,this.observationTimeoutMs=t.observationTimeoutMs??13e4,this.cancellationTerminalObservationTimeoutMs=t.cancellationTerminalObservationTimeoutMs??3e4,this.setTimer=t.setTimer??((e,r)=>{const o=setTimeout(e,r);return o.unref?.(),o}),this.clearTimer=t.clearTimer??(e=>clearTimeout(e)),this.onRecoveryRequired=t.onRecoveryRequired}tryStartInteractive(e,r){const o=this.gate.tryStartInteractive(e);return o.ok&&this.register(o.token,r,"attachable"),o}tryStartExclusive(e,r){const o=this.gate.tryStartInteractive(e);return o.ok&&this.register(o.token,r,"exclusive"),o}async acquireInternal(e){const r=await this.gate.acquireInternal(e);return this.register(r,"internal","attachable"),r}bindRun(e,r){if(!e||!r)return;const o=this.states.get(e.operationId);o&&"recovery_required"!==o.phase&&(o.runId=r)}attachOrStartExternal(e,r,o){const t=this.stateForSession(e.sessionKey);if(t)return t.recovery?{ok:!1,reason:"session_busy",recovery:t.recovery}:"exclusive"===t.attachmentPolicy?{ok:!1,reason:"session_busy"}:t.runId&&o&&t.runId!==o?{ok:!1,reason:"run_conflict",recovery:this.fence(t,"run_conflict")}:(!t.runId&&o&&(t.runId=o),{ok:!0,ownership:"observer",ownerOperationId:t.token.operationId,attached:!0});const n=this.tryStartInteractive(e,r);return n.ok?(o&&this.bindRun(n.token,o),{ok:!0,ownership:"owner",token:n.token,attached:!1}):{ok:!1,reason:n.reason}}requestCancel(e,r){const o=this.requireState(e);if(o.recovery)return Promise.resolve({ok:!1,recovery:o.recovery});if(o.cancelPromise)return o.cancelPromise;if(o.terminalKind||o.convergencePromise)return Promise.resolve({ok:!1,reason:"terminal_already_observed"});o.phase="cancelling",o.cancelObservationPromise=new Promise(e=>{o.resolveCancelObservation=e});const t=o.cancelObservationPromise;o.cancelObservationTimer=this.setTimer(()=>{o.terminalKind||o.recovery||this.fence(o,"terminal_not_observed")},this.cancellationTerminalObservationTimeoutMs);const n=Promise.resolve().then(()=>r.chatAbort(e.sessionKey,o.runId)).then(r=>(r.aborted?(o.cancelAcknowledged=!0,this.logger.debug?.(`[session-mutation] remote abort acknowledged sessionKey=${e.sessionKey} operationId=${e.operationId} runId=${o.runId??""} returnedRunIds=${r.runIds.join(",")}`)):this.logger.debug?.(`[session-mutation] remote abort not acknowledged; continuing terminal/quiescence convergence sessionKey=${e.sessionKey} operationId=${e.operationId} runId=${o.runId??""} returnedRunIds=${r.runIds.join(",")}`),o.recovery?{ok:!1,recovery:o.recovery}:{ok:!0,acknowledged:r.aborted,terminalObservation:t})).catch(r=>(this.logger.error?.(`[session-mutation] remote abort transport failed; continuing terminal/quiescence convergence sessionKey=${e.sessionKey} operationId=${e.operationId} runId=${o.runId??""} error=${r instanceof Error?r.message:String(r)}`),o.recovery?{ok:!1,recovery:o.recovery}:{ok:!0,acknowledged:!1,terminalObservation:t}));return o.cancelPromise=n,n}requestCancelForSession(e,r){const o=this.stateForSession(e);return o?"exclusive"===o.attachmentPolicy?Promise.resolve({ok:!1,reason:"not_cancellable"}):this.requestCancel(o.token,r):Promise.resolve({ok:!1,reason:"no_active_mutation"})}converge(e,r){const o=this.requireState(e);return o.convergencePromise||(o.terminalKind=r,this.resolveTerminalObserved(o),o.convergencePromise=this.proveConvergence(o)),o.convergencePromise}release(e){const r=this.states.get(e.operationId);r&&r.token.sessionKey===e.sessionKey&&"ready_to_release"===r.phase&&(this.clearCancellationTimer(r),this.gate.release(r.token),this.states.delete(e.operationId))}releaseBeforeRuntime(e){if(!e)return;const r=this.states.get(e.operationId);r?.recovery||(r&&this.clearCancellationTimer(r),this.gate.release(e),this.states.delete(e.operationId))}requireRecovery(e,r,o){const t=this.requireState(e);return o&&(t.terminalKind=o),this.fence(t,r)}snapshot(e){for(const r of this.states.values())if(r.token.sessionKey===e)return{sessionKey:e,operationId:r.token.operationId,surface:r.surface,attachmentPolicy:r.attachmentPolicy,phase:r.phase,...r.runId?{runId:r.runId}:{},...r.terminalKind?{terminalKind:r.terminalKind}:{},cancelAcknowledged:r.cancelAcknowledged,...r.recovery?{recovery:r.recovery}:{}}}gateSnapshot(e){return this.gate.snapshot(e)}fenceActive(e){for(const r of this.states.values())r.recovery||this.fence(r,e)}register(e,r,o){this.states.set(e.operationId,{token:e,surface:r,attachmentPolicy:o,phase:"active",cancelAcknowledged:!1})}requireState(e){const r=this.states.get(e.operationId);if(!r||r.token.sessionKey!==e.sessionKey)throw new Error("session_mutation_not_registered");return r}stateForSession(e){for(const r of this.states.values())if(r.token.sessionKey===e)return r}async proveConvergence(e){if(e.recovery)return{ok:!1,recovery:e.recovery};if(!e.terminalKind)return{ok:!1,recovery:this.fence(e,"terminal_not_observed")};if(e.recovery)return{ok:!1,recovery:e.recovery};
|
|
1
|
+
export const SESSION_RECOVERY_REQUIRED_CODE="session_recovery_required";export const SESSION_RECOVERY_REQUIRED_MESSAGE="The session could not be proven safe for another write. Recovery is required.";const DEFAULT_OBSERVATION_TIMEOUT_MS=13e4,DEFAULT_CANCELLATION_QUIESCENCE_TIMEOUT_MS=3e4,DEFAULT_CANCELLATION_TERMINAL_OBSERVATION_TIMEOUT_MS=3e4;export class SessionMutationCoordinator{gate;quiescence;logger;states=new Map;observationTimeoutMs;cancellationQuiescenceTimeoutMs;cancellationTerminalObservationTimeoutMs;setTimer;clearTimer;onRecoveryRequired;constructor(e,r,o,t={}){this.gate=e,this.quiescence=r,this.logger=o,this.observationTimeoutMs=t.observationTimeoutMs??13e4,this.cancellationQuiescenceTimeoutMs=t.cancellationQuiescenceTimeoutMs??3e4,this.cancellationTerminalObservationTimeoutMs=t.cancellationTerminalObservationTimeoutMs??3e4,this.setTimer=t.setTimer??((e,r)=>{const o=setTimeout(e,r);return o.unref?.(),o}),this.clearTimer=t.clearTimer??(e=>clearTimeout(e)),this.onRecoveryRequired=t.onRecoveryRequired}tryStartInteractive(e,r){const o=this.gate.tryStartInteractive(e);return o.ok&&this.register(o.token,r,"attachable"),o}tryStartExclusive(e,r){const o=this.gate.tryStartInteractive(e);return o.ok&&this.register(o.token,r,"exclusive"),o}async acquireInternal(e){const r=await this.gate.acquireInternal(e);return this.register(r,"internal","attachable"),r}bindRun(e,r){if(!e||!r)return;const o=this.states.get(e.operationId);o&&"recovery_required"!==o.phase&&(o.runId=r)}attachOrStartExternal(e,r,o){const t=this.stateForSession(e.sessionKey);if(t)return t.recovery?{ok:!1,reason:"session_busy",recovery:t.recovery}:"exclusive"===t.attachmentPolicy?{ok:!1,reason:"session_busy"}:t.runId&&o&&t.runId!==o?{ok:!1,reason:"run_conflict",recovery:this.fence(t,"run_conflict")}:(!t.runId&&o&&(t.runId=o),{ok:!0,ownership:"observer",ownerOperationId:t.token.operationId,attached:!0});const n=this.tryStartInteractive(e,r);return n.ok?(o&&this.bindRun(n.token,o),{ok:!0,ownership:"owner",token:n.token,attached:!1}):{ok:!1,reason:n.reason}}requestCancel(e,r){const o=this.requireState(e);if(o.recovery)return Promise.resolve({ok:!1,recovery:o.recovery});if(o.cancelPromise)return o.cancelPromise;if(o.terminalKind||o.convergencePromise)return Promise.resolve({ok:!1,reason:"terminal_already_observed"});o.phase="cancelling",o.cancelObservationPromise=new Promise(e=>{o.resolveCancelObservation=e});const t=o.cancelObservationPromise;o.cancelObservationTimer=this.setTimer(()=>{o.terminalKind||o.recovery||this.fence(o,"terminal_not_observed")},this.cancellationTerminalObservationTimeoutMs);const n=Promise.resolve().then(()=>r.chatAbort(e.sessionKey,o.runId)).then(r=>(r.aborted?(o.cancelAcknowledged=!0,this.logger.debug?.(`[session-mutation] remote abort acknowledged sessionKey=${e.sessionKey} operationId=${e.operationId} runId=${o.runId??""} returnedRunIds=${r.runIds.join(",")}`)):this.logger.debug?.(`[session-mutation] remote abort not acknowledged; continuing terminal/quiescence convergence sessionKey=${e.sessionKey} operationId=${e.operationId} runId=${o.runId??""} returnedRunIds=${r.runIds.join(",")}`),o.recovery?{ok:!1,recovery:o.recovery}:{ok:!0,acknowledged:r.aborted,terminalObservation:t})).catch(r=>(this.logger.error?.(`[session-mutation] remote abort transport failed; continuing terminal/quiescence convergence sessionKey=${e.sessionKey} operationId=${e.operationId} runId=${o.runId??""} error=${r instanceof Error?r.message:String(r)}`),o.recovery?{ok:!1,recovery:o.recovery}:{ok:!0,acknowledged:!1,terminalObservation:t}));return o.cancelPromise=n,n}requestCancelForSession(e,r){const o=this.stateForSession(e);return o?"exclusive"===o.attachmentPolicy?Promise.resolve({ok:!1,reason:"not_cancellable"}):this.requestCancel(o.token,r):Promise.resolve({ok:!1,reason:"no_active_mutation"})}converge(e,r){const o=this.requireState(e);return o.convergencePromise||(o.terminalKind=r,this.resolveTerminalObserved(o),o.convergencePromise=this.proveConvergence(o)),o.convergencePromise}release(e){const r=this.states.get(e.operationId);r&&r.token.sessionKey===e.sessionKey&&"ready_to_release"===r.phase&&(this.clearCancellationTimer(r),this.gate.release(r.token),this.states.delete(e.operationId))}releaseBeforeRuntime(e){if(!e)return;const r=this.states.get(e.operationId);r?.recovery||(r&&this.clearCancellationTimer(r),this.gate.release(e),this.states.delete(e.operationId))}requireRecovery(e,r,o){const t=this.requireState(e);return o&&(t.terminalKind=o),this.fence(t,r)}snapshot(e){for(const r of this.states.values())if(r.token.sessionKey===e)return{sessionKey:e,operationId:r.token.operationId,surface:r.surface,attachmentPolicy:r.attachmentPolicy,phase:r.phase,...r.runId?{runId:r.runId}:{},...r.terminalKind?{terminalKind:r.terminalKind}:{},cancelAcknowledged:r.cancelAcknowledged,...r.recovery?{recovery:r.recovery}:{}}}gateSnapshot(e){return this.gate.snapshot(e)}fenceActive(e){for(const r of this.states.values())r.recovery||this.fence(r,e)}register(e,r,o){this.states.set(e.operationId,{token:e,surface:r,attachmentPolicy:o,phase:"active",cancelAcknowledged:!1})}requireState(e){const r=this.states.get(e.operationId);if(!r||r.token.sessionKey!==e.sessionKey)throw new Error("session_mutation_not_registered");return r}stateForSession(e){for(const r of this.states.values())if(r.token.sessionKey===e)return r}async proveConvergence(e){if(e.recovery)return{ok:!1,recovery:e.recovery};if(!e.terminalKind)return{ok:!1,recovery:this.fence(e,"terminal_not_observed")};if(e.recovery)return{ok:!1,recovery:e.recovery};e.phase="awaiting_quiescence";const r="cancelled"===e.terminalKind?this.cancellationQuiescenceTimeoutMs:this.observationTimeoutMs;let o;try{o=await this.quiescence.waitForQuiescence(e.token.sessionKey,r)}catch{return e.recovery?{ok:!1,recovery:e.recovery}:{ok:!1,recovery:this.fence(e,"quiescence_failed")}}if(e.recovery)return{ok:!1,recovery:e.recovery};if(!o.quiescent){const r="session_unresolved"===o.reason?"session_unresolved":"quiescence_timeout";return{ok:!1,recovery:this.fence(e,r,o)}}return e.phase="ready_to_release",this.logger.debug?.(`[session-mutation] convergence proven sessionKey=${e.token.sessionKey} operationId=${e.token.operationId} terminal=${e.terminalKind} elapsed_ms=${o.elapsedMs}`),{ok:!0,permit:{sessionKey:e.token.sessionKey,operationId:e.token.operationId}}}fence(e,r,o){if(e.recovery)return e.recovery;const t={code:"session_recovery_required",sessionKey:e.token.sessionKey,operationId:e.token.operationId,reason:r,surface:e.surface,...e.terminalKind?{terminalKind:e.terminalKind}:{},...o?{quiescence:o}:{}};e.recovery=t,e.phase="recovery_required",this.clearCancellationTimer(e),e.resolveCancelObservation?.({ok:!1,recovery:t}),e.resolveCancelObservation=void 0,this.logger.error?.(`[session-mutation] recovery required sessionKey=${t.sessionKey} operationId=${t.operationId} reason=${t.reason} surface=${t.surface}`);try{this.onRecoveryRequired?.(t)}catch(e){this.logger.error?.(`[session-mutation] recovery restart scheduling failed sessionKey=${t.sessionKey} operationId=${t.operationId} error=${e instanceof Error?e.message:String(e)}`)}return t}resolveTerminalObserved(e){e.recovery||(this.clearCancellationTimer(e),e.resolveCancelObservation?.({ok:!0,terminalObserved:!0}),e.resolveCancelObservation=void 0)}clearCancellationTimer(e){e.cancelObservationTimer&&(this.clearTimer(e.cancelObservationTimer),e.cancelObservationTimer=void 0)}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{describe,expect,it,vi}from"vitest";import{SessionWriteGate}from"../acp-gateway/session-write-gate.js";import{SessionMutationCoordinator}from"./session-mutation-coordinator.js";const deferred=()=>{let e;return{promise:new Promise(t=>{e=t}),resolve:e}};describe("SessionMutationCoordinator",()=>{it("allows terminal quiescence to release before the remote abort observation returns",async()=>{const e=new SessionWriteGate,t=deferred(),n=deferred(),a=new SessionMutationCoordinator(e,{waitForQuiescence:vi.fn(async()=>(await t.promise,{quiescent:!0,reason:"unlocked",elapsedMs:5}))},{}),o=a.tryStartInteractive({sessionKey:"agent:main:one"},"acp");if(!o.ok)throw new Error("expected lease");a.bindRun(o.token,"run-1");const s=a.requestCancel(o.token,{chatAbort:vi.fn(async()=>(await n.promise,{aborted:!0,runIds:["run-1"]}))}),r=a.converge(o.token,"cancelled");await Promise.resolve(),expect(a.snapshot("agent:main:one")?.cancelAcknowledged).toBe(!1),expect(a.snapshot("agent:main:one")?.phase).toBe("awaiting_quiescence"),expect(e.tryStartInteractive({sessionKey:"agent:main:one"}).ok).toBe(!1),t.resolve();const c=await r;expect(c.ok).toBe(!0),expect(e.tryStartInteractive({sessionKey:"agent:main:one"}).ok).toBe(!1),c.ok&&a.release(c.permit),expect(e.tryStartInteractive({sessionKey:"agent:main:one"}).ok).toBe(!0),n.resolve(),await expect(s).resolves.toMatchObject({ok:!0,acknowledged:!0})}),it("fails closed and exposes recovery-required when quiescence times out",async()=>{const e=new SessionWriteGate,t=vi.fn(),n=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!1,reason:"timeout",elapsedMs:20})},{},{observationTimeoutMs:20,onRecoveryRequired:t}),a=n.tryStartInteractive({sessionKey:"agent:main:fenced"},"message_service");if(!a.ok)throw new Error("expected lease");await expect(n.converge(a.token,"timeout")).resolves.toMatchObject({ok:!1,recovery:{code:"session_recovery_required",reason:"quiescence_timeout",terminalKind:"timeout"}}),expect(n.snapshot("agent:main:fenced")?.phase).toBe("recovery_required"),expect(e.tryStartInteractive({sessionKey:"agent:main:fenced"}).ok).toBe(!1),expect(t).toHaveBeenCalledOnce(),expect(t).toHaveBeenCalledWith(expect.objectContaining({reason:"quiescence_timeout",sessionKey:"agent:main:fenced"}))}),it("fences as quiescence_failed when the observer rejects",async()=>{const e=new SessionWriteGate,t=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>{throw new Error("observer failed")}},{}),n=t.tryStartInteractive({sessionKey:"agent:main:observer-failed"},"acp");if(!n.ok)throw new Error("expected lease");await expect(t.converge(n.token,"error")).resolves.toMatchObject({ok:!1,recovery:{code:"session_recovery_required",reason:"quiescence_failed"}}),expect(t.snapshot("agent:main:observer-failed")?.phase).toBe("recovery_required"),expect(e.tryStartInteractive({sessionKey:"agent:main:observer-failed"}).ok).toBe(!1)}),it("blocks the same gateway session while allowing different sessions concurrently",()=>{const e=new SessionWriteGate,t=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{});expect(t.tryStartInteractive({sessionKey:"agent:main:a"},"acp").ok).toBe(!0),expect(t.tryStartInteractive({sessionKey:"agent:main:a"},"message_service").ok).toBe(!1),expect(t.tryStartInteractive({sessionKey:"agent:main:b"},"message_service").ok).toBe(!0)}),it("releases a normally completed mutation only after the caller commits its permit",async()=>{const e=new SessionWriteGate,t=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{}),n=t.tryStartInteractive({sessionKey:"agent:main:normal"},"acp");if(!n.ok)throw new Error("expected lease");const a=await t.converge(n.token,"completed");expect(a.ok).toBe(!0),expect(e.snapshot("agent:main:normal").active).toBeDefined(),a.ok&&t.release(a.permit),expect(e.snapshot("agent:main:normal").active).toBeUndefined()}),it.each(["acp","message_service"])("applies identical cancel convergence for the %s surface",async e=>{const t=new SessionWriteGate,n=vi.fn(),a=vi.fn().mockResolvedValue({aborted:!0,runIds:["run-shared-contract"]}),o=new SessionMutationCoordinator(t,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:1})},{},{onRecoveryRequired:n}),s=o.tryStartInteractive({sessionKey:`agent:main:${e}`},e);if(!s.ok)throw new Error("expected lease");o.bindRun(s.token,"run-shared-contract"),await expect(o.requestCancel(s.token,{chatAbort:a})).resolves.toMatchObject({ok:!0,acknowledged:!0});const r=await o.converge(s.token,"cancelled");expect(r.ok).toBe(!0),expect(a).toHaveBeenCalledWith(`agent:main:${e}`,"run-shared-contract"),expect(o.snapshot(`agent:main:${e}`)).toMatchObject({surface:e,phase:"ready_to_release",terminalKind:"cancelled",cancelAcknowledged:!0}),expect(n).not.toHaveBeenCalled()}),it("treats aborted=false as observation and releases after terminal quiescence",async()=>{const e=new SessionWriteGate,t=vi.fn(),n=vi.fn(),a=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{debug:n},{onRecoveryRequired:t}),o=a.tryStartInteractive({sessionKey:"agent:main:abort-missed"},"message_service");if(!o.ok)throw new Error("expected lease");a.bindRun(o.token,"run-missed");const s={chatAbort:vi.fn().mockResolvedValue({aborted:!1,runIds:[]})};await expect(a.requestCancel(o.token,s)).resolves.toMatchObject({ok:!0,acknowledged:!1}),expect(a.snapshot("agent:main:abort-missed")).toMatchObject({phase:"cancelling",cancelAcknowledged:!1}),expect(n).toHaveBeenCalledWith(expect.stringContaining("remote abort not acknowledged; continuing terminal/quiescence convergence"));const r=await a.converge(o.token,"cancelled");expect(r.ok).toBe(!0),expect(a.snapshot("agent:main:abort-missed")).toMatchObject({phase:"ready_to_release",terminalKind:"cancelled",cancelAcknowledged:!1}),expect(t).not.toHaveBeenCalled(),r.ok&&a.release(r.permit),expect(e.snapshot("agent:main:abort-missed").active).toBeUndefined(),expect(s.chatAbort).toHaveBeenCalledTimes(1)}),it("treats remote abort transport errors as observation and still converges",async()=>{const e=new SessionWriteGate,t=vi.fn(),n=vi.fn(),a=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:1})},{error:n},{onRecoveryRequired:t}),o=a.tryStartInteractive({sessionKey:"agent:main:abort-transport-error"},"message_service");if(!o.ok)throw new Error("expected lease");await expect(a.requestCancel(o.token,{chatAbort:async()=>{throw new Error("gateway unavailable")}})).resolves.toMatchObject({ok:!0,acknowledged:!1}),expect(n).toHaveBeenCalledWith(expect.stringContaining("remote abort transport failed; continuing terminal/quiescence convergence"));const s=await a.converge(o.token,"cancelled");expect(s.ok).toBe(!0),expect(t).not.toHaveBeenCalled(),s.ok&&a.release(s.permit)}),it("restarts once when aborted=false is not followed by a terminal",async()=>{const e=new SessionWriteGate,t=vi.fn();let n;const a=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{},{cancellationTerminalObservationTimeoutMs:3e4,onRecoveryRequired:t,setTimer:e=>(n=e,{unref:()=>{}}),clearTimer:()=>{}}),o=a.tryStartInteractive({sessionKey:"agent:main:false-no-terminal"},"message_service");if(!o.ok)throw new Error("expected lease");const s=await a.requestCancel(o.token,{chatAbort:async()=>({aborted:!1,runIds:[]})});if(!s.ok)throw new Error("expected accepted cancel");expect(s.acknowledged).toBe(!1),n(),n(),await expect(s.terminalObservation).resolves.toMatchObject({ok:!1,recovery:{reason:"terminal_not_observed"}}),expect(t).toHaveBeenCalledOnce()}),it("restarts once when aborted=false reaches terminal but lock quiescence times out",async()=>{const e=new SessionWriteGate,t=vi.fn(),n=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!1,reason:"timeout",elapsedMs:13e4})},{},{observationTimeoutMs:13e4,onRecoveryRequired:t}),a=n.tryStartInteractive({sessionKey:"agent:main:false-lock-timeout"},"message_service");if(!a.ok)throw new Error("expected lease");await expect(n.requestCancel(a.token,{chatAbort:async()=>({aborted:!1,runIds:[]})})).resolves.toMatchObject({ok:!0,acknowledged:!1}),await expect(n.converge(a.token,"cancelled")).resolves.toMatchObject({ok:!1,recovery:{reason:"quiescence_timeout"}}),expect(t).toHaveBeenCalledOnce()}),it("requests cancellation from the current owner by session key",async()=>{const e=new SessionWriteGate,t=vi.fn().mockResolvedValue({aborted:!0,runIds:["run-owner"]}),n=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{}),a=n.tryStartInteractive({sessionKey:"agent:main:cancel-by-session"},"message_service");if(!a.ok)throw new Error("expected owner");n.bindRun(a.token,"run-owner"),await expect(n.requestCancelForSession("agent:main:cancel-by-session",{chatAbort:t})).resolves.toMatchObject({ok:!0,acknowledged:!0}),expect(t).toHaveBeenCalledWith("agent:main:cancel-by-session","run-owner"),expect(n.snapshot("agent:main:cancel-by-session")).toMatchObject({operationId:a.token.operationId,phase:"cancelling",cancelAcknowledged:!0});const o=await n.converge(a.token,"cancelled");o.ok&&n.release(o.permit)}),it("reports no active mutation when cancellation has no owner",async()=>{const e=new SessionMutationCoordinator(new SessionWriteGate,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{});await expect(e.requestCancelForSession("agent:main:no-owner",{chatAbort:vi.fn()})).resolves.toEqual({ok:!1,reason:"no_active_mutation"})}),it("fences and resolves recovery when cancel ACK is not followed by a terminal",async()=>{const e=new SessionWriteGate;let t;const n=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{},{cancellationTerminalObservationTimeoutMs:25,setTimer:e=>(t=e,{unref:()=>{}}),clearTimer:()=>{}}),a=n.tryStartInteractive({sessionKey:"agent:main:no-terminal"},"acp");if(!a.ok)throw new Error("expected lease");const o=await n.requestCancel(a.token,{chatAbort:async()=>({aborted:!0,runIds:[]})});if(!o.ok)throw new Error("expected cancel ACK");t(),await expect(o.terminalObservation).resolves.toMatchObject({ok:!1,recovery:{code:"session_recovery_required",reason:"terminal_not_observed"}}),expect(n.snapshot("agent:main:no-terminal")?.phase).toBe("recovery_required"),expect(e.tryStartInteractive({sessionKey:"agent:main:no-terminal"}).ok).toBe(!1)}),it("clears the cancel observation timer when terminal arrives",async()=>{const e=new SessionWriteGate,t={unref:()=>{}},n=vi.fn(),a=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{},{cancellationTerminalObservationTimeoutMs:25,setTimer:()=>t,clearTimer:n}),o=a.tryStartInteractive({sessionKey:"agent:main:terminal"},"message_service");if(!o.ok)throw new Error("expected lease");const s=await a.requestCancel(o.token,{chatAbort:async()=>({aborted:!0,runIds:[]})});if(!s.ok)throw new Error("expected cancel ACK");const r=a.converge(o.token,"cancelled");await expect(s.terminalObservation).resolves.toEqual({ok:!0,terminalObserved:!0}),expect(n).toHaveBeenCalledWith(t),await expect(r).resolves.toMatchObject({ok:!0})}),it("rejects late cancel after terminal convergence has started",async()=>{const e=new SessionWriteGate,t=deferred(),n=vi.fn().mockResolvedValue({aborted:!0,runIds:[]}),a=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>(await t.promise,{quiescent:!0,reason:"unlocked",elapsedMs:1})},{}),o=a.tryStartInteractive({sessionKey:"agent:main:late-cancel"},"acp");if(!o.ok)throw new Error("expected lease");const s=a.converge(o.token,"completed");await expect(a.requestCancel(o.token,{chatAbort:n})).resolves.toEqual({ok:!1,reason:"terminal_already_observed"}),expect(n).not.toHaveBeenCalled(),t.resolve(),await expect(s).resolves.toMatchObject({ok:!0})}),it("keeps disconnect recovery fenced when quiescence resolves concurrently",async()=>{const e=new SessionWriteGate,t=deferred(),n=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>(await t.promise,{quiescent:!0,reason:"unlocked",elapsedMs:1})},{}),a=n.tryStartInteractive({sessionKey:"agent:main:disconnect-race"},"acp");if(!a.ok)throw new Error("expected lease");const o=n.converge(a.token,"completed");await Promise.resolve(),n.fenceActive("gateway_disconnected"),t.resolve(),await expect(o).resolves.toMatchObject({ok:!1,recovery:{reason:"gateway_disconnected"}}),expect(n.snapshot("agent:main:disconnect-race")?.phase).toBe("recovery_required"),expect(e.tryStartInteractive({sessionKey:"agent:main:disconnect-race"}).ok).toBe(!1)}),it.each([void 0,"run-shared"])("attaches an external run to an existing Message Service owner (owner run=%s)",e=>{const t=new SessionWriteGate,n=new SessionMutationCoordinator(t,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{}),a=n.tryStartInteractive({sessionKey:"agent:main:shared-owner"},"message_service");if(!a.ok)throw new Error("expected owner");e&&n.bindRun(a.token,e);const o=n.attachOrStartExternal({sessionKey:"agent:main:shared-owner"},"acp","run-shared");expect(o).toEqual({ok:!0,ownership:"observer",ownerOperationId:a.token.operationId,attached:!0}),expect(n.snapshot("agent:main:shared-owner")?.runId).toBe("run-shared")}),it("keeps the owner lease active when an attached observer reaches terminal first",async()=>{const e=new SessionWriteGate,t=vi.fn(async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})),n=new SessionMutationCoordinator(e,{waitForQuiescence:t},{}),a=n.attachOrStartExternal({sessionKey:"agent:main:observer-terminal"},"message_service","run-shared");if(!a.ok||"owner"!==a.ownership)throw new Error("expected owner");const o=n.attachOrStartExternal({sessionKey:"agent:main:observer-terminal"},"acp","run-shared");expect(o).toMatchObject({ok:!0,ownership:"observer",attached:!0}),expect(n.snapshot("agent:main:observer-terminal")?.phase).toBe("active"),expect(e.snapshot("agent:main:observer-terminal").active?.operationId).toBe(a.token.operationId),expect(t).not.toHaveBeenCalled();const s=await n.converge(a.token,"completed");expect(s.ok).toBe(!0),s.ok&&n.release(s.permit),expect(e.snapshot("agent:main:observer-terminal").active).toBeUndefined()}),it("returns an owner token when no mutation exists",()=>{const e=new SessionWriteGate,t=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{}).attachOrStartExternal({sessionKey:"agent:main:new-external"},"acp","run-new");expect(t).toMatchObject({ok:!0,ownership:"owner",attached:!1,token:{sessionKey:"agent:main:new-external"}})}),it("rejects external attachment and cancellation without mutating an exclusive owner",async()=>{const e=new SessionWriteGate,t=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{}),n=t.tryStartExclusive({sessionKey:"agent:main:exclusive"},"acp");if(!n.ok)throw new Error("expected exclusive owner");expect(t.attachOrStartExternal({sessionKey:"agent:main:exclusive"},"message_service","run-external")).toEqual({ok:!1,reason:"session_busy"}),expect(t.snapshot("agent:main:exclusive")).toMatchObject({operationId:n.token.operationId,attachmentPolicy:"exclusive",phase:"active"}),expect(t.snapshot("agent:main:exclusive")?.runId).toBeUndefined(),expect(e.snapshot("agent:main:exclusive").active?.operationId).toBe(n.token.operationId);const a=vi.fn();await expect(t.requestCancelForSession("agent:main:exclusive",{chatAbort:a})).resolves.toEqual({ok:!1,reason:"not_cancellable"}),expect(a).not.toHaveBeenCalled(),expect(t.snapshot("agent:main:exclusive")).toMatchObject({operationId:n.token.operationId,attachmentPolicy:"exclusive",phase:"active"})}),it("fences an existing owner when an external run id conflicts",()=>{const e=new SessionWriteGate,t=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{}),n=t.tryStartInteractive({sessionKey:"agent:main:conflict"},"message_service");if(!n.ok)throw new Error("expected owner");t.bindRun(n.token,"run-owner"),expect(t.attachOrStartExternal({sessionKey:"agent:main:conflict"},"acp","run-external")).toMatchObject({ok:!1,reason:"run_conflict",recovery:{reason:"run_conflict"}}),expect(t.snapshot("agent:main:conflict")?.phase).toBe("recovery_required")})});
|
|
1
|
+
import{describe,expect,it,vi}from"vitest";import{SessionWriteGate}from"../acp-gateway/session-write-gate.js";import{SessionMutationCoordinator}from"./session-mutation-coordinator.js";const deferred=()=>{let e;return{promise:new Promise(t=>{e=t}),resolve:e}};describe("SessionMutationCoordinator",()=>{it("allows terminal quiescence to release before the remote abort observation returns",async()=>{const e=new SessionWriteGate,t=deferred(),n=deferred(),a=new SessionMutationCoordinator(e,{waitForQuiescence:vi.fn(async()=>(await t.promise,{quiescent:!0,reason:"unlocked",elapsedMs:5}))},{}),o=a.tryStartInteractive({sessionKey:"agent:main:one"},"acp");if(!o.ok)throw new Error("expected lease");a.bindRun(o.token,"run-1");const s=a.requestCancel(o.token,{chatAbort:vi.fn(async()=>(await n.promise,{aborted:!0,runIds:["run-1"]}))}),r=a.converge(o.token,"cancelled");await Promise.resolve(),expect(a.snapshot("agent:main:one")?.cancelAcknowledged).toBe(!1),expect(a.snapshot("agent:main:one")?.phase).toBe("awaiting_quiescence"),expect(e.tryStartInteractive({sessionKey:"agent:main:one"}).ok).toBe(!1),t.resolve();const i=await r;expect(i.ok).toBe(!0),expect(e.tryStartInteractive({sessionKey:"agent:main:one"}).ok).toBe(!1),i.ok&&a.release(i.permit),expect(e.tryStartInteractive({sessionKey:"agent:main:one"}).ok).toBe(!0),n.resolve(),await expect(s).resolves.toMatchObject({ok:!0,acknowledged:!0})}),it("fails closed and exposes recovery-required when quiescence times out",async()=>{const e=new SessionWriteGate,t=vi.fn(),n=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!1,reason:"timeout",elapsedMs:20})},{},{observationTimeoutMs:20,onRecoveryRequired:t}),a=n.tryStartInteractive({sessionKey:"agent:main:fenced"},"message_service");if(!a.ok)throw new Error("expected lease");await expect(n.converge(a.token,"timeout")).resolves.toMatchObject({ok:!1,recovery:{code:"session_recovery_required",reason:"quiescence_timeout",terminalKind:"timeout"}}),expect(n.snapshot("agent:main:fenced")?.phase).toBe("recovery_required"),expect(e.tryStartInteractive({sessionKey:"agent:main:fenced"}).ok).toBe(!1),expect(t).toHaveBeenCalledOnce(),expect(t).toHaveBeenCalledWith(expect.objectContaining({reason:"quiescence_timeout",sessionKey:"agent:main:fenced"}))}),it("fences as quiescence_failed when the observer rejects",async()=>{const e=new SessionWriteGate,t=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>{throw new Error("observer failed")}},{}),n=t.tryStartInteractive({sessionKey:"agent:main:observer-failed"},"acp");if(!n.ok)throw new Error("expected lease");await expect(t.converge(n.token,"error")).resolves.toMatchObject({ok:!1,recovery:{code:"session_recovery_required",reason:"quiescence_failed"}}),expect(t.snapshot("agent:main:observer-failed")?.phase).toBe("recovery_required"),expect(e.tryStartInteractive({sessionKey:"agent:main:observer-failed"}).ok).toBe(!1)}),it("blocks the same gateway session while allowing different sessions concurrently",()=>{const e=new SessionWriteGate,t=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{});expect(t.tryStartInteractive({sessionKey:"agent:main:a"},"acp").ok).toBe(!0),expect(t.tryStartInteractive({sessionKey:"agent:main:a"},"message_service").ok).toBe(!1),expect(t.tryStartInteractive({sessionKey:"agent:main:b"},"message_service").ok).toBe(!0)}),it("releases a normally completed mutation only after the caller commits its permit",async()=>{const e=new SessionWriteGate,t=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{}),n=t.tryStartInteractive({sessionKey:"agent:main:normal"},"acp");if(!n.ok)throw new Error("expected lease");const a=await t.converge(n.token,"completed");expect(a.ok).toBe(!0),expect(e.snapshot("agent:main:normal").active).toBeDefined(),a.ok&&t.release(a.permit),expect(e.snapshot("agent:main:normal").active).toBeUndefined()}),it.each(["acp","message_service"])("applies identical cancel convergence for the %s surface",async e=>{const t=new SessionWriteGate,n=vi.fn(),a=vi.fn().mockResolvedValue({aborted:!0,runIds:["run-shared-contract"]}),o=new SessionMutationCoordinator(t,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:1})},{},{onRecoveryRequired:n}),s=o.tryStartInteractive({sessionKey:`agent:main:${e}`},e);if(!s.ok)throw new Error("expected lease");o.bindRun(s.token,"run-shared-contract"),await expect(o.requestCancel(s.token,{chatAbort:a})).resolves.toMatchObject({ok:!0,acknowledged:!0});const r=await o.converge(s.token,"cancelled");expect(r.ok).toBe(!0),expect(a).toHaveBeenCalledWith(`agent:main:${e}`,"run-shared-contract"),expect(o.snapshot(`agent:main:${e}`)).toMatchObject({surface:e,phase:"ready_to_release",terminalKind:"cancelled",cancelAcknowledged:!0}),expect(n).not.toHaveBeenCalled()}),it("treats aborted=false as observation and releases after terminal quiescence",async()=>{const e=new SessionWriteGate,t=vi.fn(),n=vi.fn(),a=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{debug:n},{onRecoveryRequired:t}),o=a.tryStartInteractive({sessionKey:"agent:main:abort-missed"},"message_service");if(!o.ok)throw new Error("expected lease");a.bindRun(o.token,"run-missed");const s={chatAbort:vi.fn().mockResolvedValue({aborted:!1,runIds:[]})};await expect(a.requestCancel(o.token,s)).resolves.toMatchObject({ok:!0,acknowledged:!1}),expect(a.snapshot("agent:main:abort-missed")).toMatchObject({phase:"cancelling",cancelAcknowledged:!1}),expect(n).toHaveBeenCalledWith(expect.stringContaining("remote abort not acknowledged; continuing terminal/quiescence convergence"));const r=await a.converge(o.token,"cancelled");expect(r.ok).toBe(!0),expect(a.snapshot("agent:main:abort-missed")).toMatchObject({phase:"ready_to_release",terminalKind:"cancelled",cancelAcknowledged:!1}),expect(t).not.toHaveBeenCalled(),r.ok&&a.release(r.permit),expect(e.snapshot("agent:main:abort-missed").active).toBeUndefined(),expect(s.chatAbort).toHaveBeenCalledTimes(1)}),it("uses the shorter quiescence SLA for cancellation",async()=>{const e=new SessionWriteGate,t=vi.fn(async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})),n=new SessionMutationCoordinator(e,{waitForQuiescence:t},{},{observationTimeoutMs:13e4,cancellationQuiescenceTimeoutMs:3e4}),a=n.tryStartInteractive({sessionKey:"agent:main:cancel-sla"},"message_service");if(!a.ok)throw new Error("expected lease");await n.converge(a.token,"cancelled"),expect(t).toHaveBeenCalledWith("agent:main:cancel-sla",3e4)}),it("retains the general quiescence SLA for normal completion",async()=>{const e=new SessionWriteGate,t=vi.fn(async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})),n=new SessionMutationCoordinator(e,{waitForQuiescence:t},{},{observationTimeoutMs:13e4,cancellationQuiescenceTimeoutMs:3e4}),a=n.tryStartInteractive({sessionKey:"agent:main:completion-sla"},"message_service");if(!a.ok)throw new Error("expected lease");await n.converge(a.token,"completed"),expect(t).toHaveBeenCalledWith("agent:main:completion-sla",13e4)}),it("treats remote abort transport errors as observation and still converges",async()=>{const e=new SessionWriteGate,t=vi.fn(),n=vi.fn(),a=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:1})},{error:n},{onRecoveryRequired:t}),o=a.tryStartInteractive({sessionKey:"agent:main:abort-transport-error"},"message_service");if(!o.ok)throw new Error("expected lease");await expect(a.requestCancel(o.token,{chatAbort:async()=>{throw new Error("gateway unavailable")}})).resolves.toMatchObject({ok:!0,acknowledged:!1}),expect(n).toHaveBeenCalledWith(expect.stringContaining("remote abort transport failed; continuing terminal/quiescence convergence"));const s=await a.converge(o.token,"cancelled");expect(s.ok).toBe(!0),expect(t).not.toHaveBeenCalled(),s.ok&&a.release(s.permit)}),it("restarts once when aborted=false is not followed by a terminal",async()=>{const e=new SessionWriteGate,t=vi.fn();let n;const a=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{},{cancellationTerminalObservationTimeoutMs:3e4,onRecoveryRequired:t,setTimer:e=>(n=e,{unref:()=>{}}),clearTimer:()=>{}}),o=a.tryStartInteractive({sessionKey:"agent:main:false-no-terminal"},"message_service");if(!o.ok)throw new Error("expected lease");const s=await a.requestCancel(o.token,{chatAbort:async()=>({aborted:!1,runIds:[]})});if(!s.ok)throw new Error("expected accepted cancel");expect(s.acknowledged).toBe(!1),n(),n(),await expect(s.terminalObservation).resolves.toMatchObject({ok:!1,recovery:{reason:"terminal_not_observed"}}),expect(t).toHaveBeenCalledOnce()}),it("restarts once when aborted=false reaches terminal but lock quiescence times out",async()=>{const e=new SessionWriteGate,t=vi.fn(),n=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!1,reason:"timeout",elapsedMs:13e4})},{},{observationTimeoutMs:13e4,onRecoveryRequired:t}),a=n.tryStartInteractive({sessionKey:"agent:main:false-lock-timeout"},"message_service");if(!a.ok)throw new Error("expected lease");await expect(n.requestCancel(a.token,{chatAbort:async()=>({aborted:!1,runIds:[]})})).resolves.toMatchObject({ok:!0,acknowledged:!1}),await expect(n.converge(a.token,"cancelled")).resolves.toMatchObject({ok:!1,recovery:{reason:"quiescence_timeout"}}),expect(t).toHaveBeenCalledOnce()}),it("requests cancellation from the current owner by session key",async()=>{const e=new SessionWriteGate,t=vi.fn().mockResolvedValue({aborted:!0,runIds:["run-owner"]}),n=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{}),a=n.tryStartInteractive({sessionKey:"agent:main:cancel-by-session"},"message_service");if(!a.ok)throw new Error("expected owner");n.bindRun(a.token,"run-owner"),await expect(n.requestCancelForSession("agent:main:cancel-by-session",{chatAbort:t})).resolves.toMatchObject({ok:!0,acknowledged:!0}),expect(t).toHaveBeenCalledWith("agent:main:cancel-by-session","run-owner"),expect(n.snapshot("agent:main:cancel-by-session")).toMatchObject({operationId:a.token.operationId,phase:"cancelling",cancelAcknowledged:!0});const o=await n.converge(a.token,"cancelled");o.ok&&n.release(o.permit)}),it("reports no active mutation when cancellation has no owner",async()=>{const e=new SessionMutationCoordinator(new SessionWriteGate,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{});await expect(e.requestCancelForSession("agent:main:no-owner",{chatAbort:vi.fn()})).resolves.toEqual({ok:!1,reason:"no_active_mutation"})}),it("fences and resolves recovery when cancel ACK is not followed by a terminal",async()=>{const e=new SessionWriteGate;let t;const n=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{},{cancellationTerminalObservationTimeoutMs:25,setTimer:e=>(t=e,{unref:()=>{}}),clearTimer:()=>{}}),a=n.tryStartInteractive({sessionKey:"agent:main:no-terminal"},"acp");if(!a.ok)throw new Error("expected lease");const o=await n.requestCancel(a.token,{chatAbort:async()=>({aborted:!0,runIds:[]})});if(!o.ok)throw new Error("expected cancel ACK");t(),await expect(o.terminalObservation).resolves.toMatchObject({ok:!1,recovery:{code:"session_recovery_required",reason:"terminal_not_observed"}}),expect(n.snapshot("agent:main:no-terminal")?.phase).toBe("recovery_required"),expect(e.tryStartInteractive({sessionKey:"agent:main:no-terminal"}).ok).toBe(!1)}),it("clears the cancel observation timer when terminal arrives",async()=>{const e=new SessionWriteGate,t={unref:()=>{}},n=vi.fn(),a=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{},{cancellationTerminalObservationTimeoutMs:25,setTimer:()=>t,clearTimer:n}),o=a.tryStartInteractive({sessionKey:"agent:main:terminal"},"message_service");if(!o.ok)throw new Error("expected lease");const s=await a.requestCancel(o.token,{chatAbort:async()=>({aborted:!0,runIds:[]})});if(!s.ok)throw new Error("expected cancel ACK");const r=a.converge(o.token,"cancelled");await expect(s.terminalObservation).resolves.toEqual({ok:!0,terminalObserved:!0}),expect(n).toHaveBeenCalledWith(t),await expect(r).resolves.toMatchObject({ok:!0})}),it("rejects late cancel after terminal convergence has started",async()=>{const e=new SessionWriteGate,t=deferred(),n=vi.fn().mockResolvedValue({aborted:!0,runIds:[]}),a=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>(await t.promise,{quiescent:!0,reason:"unlocked",elapsedMs:1})},{}),o=a.tryStartInteractive({sessionKey:"agent:main:late-cancel"},"acp");if(!o.ok)throw new Error("expected lease");const s=a.converge(o.token,"completed");await expect(a.requestCancel(o.token,{chatAbort:n})).resolves.toEqual({ok:!1,reason:"terminal_already_observed"}),expect(n).not.toHaveBeenCalled(),t.resolve(),await expect(s).resolves.toMatchObject({ok:!0})}),it("keeps disconnect recovery fenced when quiescence resolves concurrently",async()=>{const e=new SessionWriteGate,t=deferred(),n=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>(await t.promise,{quiescent:!0,reason:"unlocked",elapsedMs:1})},{}),a=n.tryStartInteractive({sessionKey:"agent:main:disconnect-race"},"acp");if(!a.ok)throw new Error("expected lease");const o=n.converge(a.token,"completed");await Promise.resolve(),n.fenceActive("gateway_disconnected"),t.resolve(),await expect(o).resolves.toMatchObject({ok:!1,recovery:{reason:"gateway_disconnected"}}),expect(n.snapshot("agent:main:disconnect-race")?.phase).toBe("recovery_required"),expect(e.tryStartInteractive({sessionKey:"agent:main:disconnect-race"}).ok).toBe(!1)}),it.each([void 0,"run-shared"])("attaches an external run to an existing Message Service owner (owner run=%s)",e=>{const t=new SessionWriteGate,n=new SessionMutationCoordinator(t,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{}),a=n.tryStartInteractive({sessionKey:"agent:main:shared-owner"},"message_service");if(!a.ok)throw new Error("expected owner");e&&n.bindRun(a.token,e);const o=n.attachOrStartExternal({sessionKey:"agent:main:shared-owner"},"acp","run-shared");expect(o).toEqual({ok:!0,ownership:"observer",ownerOperationId:a.token.operationId,attached:!0}),expect(n.snapshot("agent:main:shared-owner")?.runId).toBe("run-shared")}),it("keeps the owner lease active when an attached observer reaches terminal first",async()=>{const e=new SessionWriteGate,t=vi.fn(async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})),n=new SessionMutationCoordinator(e,{waitForQuiescence:t},{}),a=n.attachOrStartExternal({sessionKey:"agent:main:observer-terminal"},"message_service","run-shared");if(!a.ok||"owner"!==a.ownership)throw new Error("expected owner");const o=n.attachOrStartExternal({sessionKey:"agent:main:observer-terminal"},"acp","run-shared");expect(o).toMatchObject({ok:!0,ownership:"observer",attached:!0}),expect(n.snapshot("agent:main:observer-terminal")?.phase).toBe("active"),expect(e.snapshot("agent:main:observer-terminal").active?.operationId).toBe(a.token.operationId),expect(t).not.toHaveBeenCalled();const s=await n.converge(a.token,"completed");expect(s.ok).toBe(!0),s.ok&&n.release(s.permit),expect(e.snapshot("agent:main:observer-terminal").active).toBeUndefined()}),it("returns an owner token when no mutation exists",()=>{const e=new SessionWriteGate,t=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{}).attachOrStartExternal({sessionKey:"agent:main:new-external"},"acp","run-new");expect(t).toMatchObject({ok:!0,ownership:"owner",attached:!1,token:{sessionKey:"agent:main:new-external"}})}),it("rejects external attachment and cancellation without mutating an exclusive owner",async()=>{const e=new SessionWriteGate,t=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{}),n=t.tryStartExclusive({sessionKey:"agent:main:exclusive"},"acp");if(!n.ok)throw new Error("expected exclusive owner");expect(t.attachOrStartExternal({sessionKey:"agent:main:exclusive"},"message_service","run-external")).toEqual({ok:!1,reason:"session_busy"}),expect(t.snapshot("agent:main:exclusive")).toMatchObject({operationId:n.token.operationId,attachmentPolicy:"exclusive",phase:"active"}),expect(t.snapshot("agent:main:exclusive")?.runId).toBeUndefined(),expect(e.snapshot("agent:main:exclusive").active?.operationId).toBe(n.token.operationId);const a=vi.fn();await expect(t.requestCancelForSession("agent:main:exclusive",{chatAbort:a})).resolves.toEqual({ok:!1,reason:"not_cancellable"}),expect(a).not.toHaveBeenCalled(),expect(t.snapshot("agent:main:exclusive")).toMatchObject({operationId:n.token.operationId,attachmentPolicy:"exclusive",phase:"active"})}),it("fences an existing owner when an external run id conflicts",()=>{const e=new SessionWriteGate,t=new SessionMutationCoordinator(e,{waitForQuiescence:async()=>({quiescent:!0,reason:"unlocked",elapsedMs:0})},{}),n=t.tryStartInteractive({sessionKey:"agent:main:conflict"},"message_service");if(!n.ok)throw new Error("expected owner");t.bindRun(n.token,"run-owner"),expect(t.attachOrStartExternal({sessionKey:"agent:main:conflict"},"acp","run-external")).toMatchObject({ok:!1,reason:"run_conflict",recovery:{reason:"run_conflict"}}),expect(t.snapshot("agent:main:conflict")?.phase).toBe("recovery_required")})});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beeos-claw",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.63",
|
|
4
4
|
"description": "OpenClaw plugin that bridges the BeeOS platform with the local OpenClaw Gateway, providing file transfer, ACP bridge, and observability.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "BeeOS <dev@beeos.ai>",
|