arcane-os 0.88.0 → 0.88.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.88.1
4
+
5
+ - Recover an ordinary workspace-operation lock when its recorded runner is
6
+ confirmed absent, including before the six-hour expiry. Retrying the public
7
+ operation uses the shared lock owner; existing published records stay readable.
8
+ - Preserve live or indeterminate owners and complete process-probe errors.
9
+ Recheck cancellation, nonce, PID and owner presence after asynchronous recovery
10
+ observation so an observed replacement remains owned by its actual writer.
11
+ The existing cooperative read/remove protocol is not an atomic cross-process
12
+ compare-and-delete.
13
+
3
14
  ## 0.88.0
4
15
 
5
16
  - Add optional `signal` and `startLanguageModel` controls to `AI.setAI`,
package/README.md CHANGED
@@ -25,7 +25,7 @@ selected SDK projection or an explicit live SDK source mount. Both profiles
25
25
  share the theme, packaging, event, cancellation,
26
26
  and browser run contracts while retaining their selected application layout.
27
27
 
28
- This checkout defines the `0.88.0` SDK contract. Development applications declare
28
+ This checkout defines the `0.88.1` SDK contract. Development applications declare
29
29
  `arcane-os` as `latest` and retain their lockfile. Exact SDK pins begin when Roshi
30
30
  designates a production build. Follow [Consumer updates](docs/consumer-updates.md)
31
31
  to refresh and compare the installed SDK and lockfile with the published channel;
@@ -405,6 +405,16 @@ not package the app or produce `dist` output. With PWA enabled, the server
405
405
  generates the installation and offline manifests directly; see
406
406
  [PWA development and versioning](pwa.md#development-and-hosting).
407
407
 
408
+ Ordinary operation acquisition can recover a workspace-operation lock whose
409
+ recorded process is confirmed absent, including before its six-hour expiry.
410
+ Retry the intended command through its usual public entrypoint; no separate
411
+ unlock command or caller-created lease is needed. A live or indeterminate owner
412
+ remains `ARCANE_WORKSPACE_BUSY`, with an indeterminate process-probe error
413
+ retained as the cause. Recovery rechecks cancellation, nonce, PID and owner
414
+ presence after asynchronous observation, preserving a replacement detected
415
+ there. This cooperative read-then-remove protocol does not provide an
416
+ atomic compare-and-delete across competing processes.
417
+
408
418
  The startup URL remains the selected app entry. At `/`, the server serves an
409
419
  existing authored host-root `index.html` and suppresses the app-entry redirect
410
420
  and PWA alias. A root outside the selected app mapping is served unchanged only
@@ -1868,6 +1868,17 @@ whole-tree replacement. The mutable result includes the installed-package
1868
1868
  location, materialized workspace runtime paths, and `workspaceLock` path and
1869
1869
  document. It does not install dependencies or merge application source.
1870
1870
 
1871
+ Ordinary acquisition automatically recovers a workspace-operation lock when
1872
+ the recorded owner process is confirmed absent, without waiting solely for its
1873
+ six-hour expiry. Retry this public operation normally; `workspaceOperationLease`
1874
+ is for active nested SDK work and cannot be reconstructed from the lock JSON.
1875
+ Live or indeterminate owners remain `ARCANE_WORKSPACE_BUSY`; an indeterminate
1876
+ process-probe error is retained as the cause. Existing published lock documents
1877
+ remain readable. Recovery rechecks cancellation, the captured nonce and PID,
1878
+ and owner presence after asynchronous observation, preserving a replacement
1879
+ detected there. This cooperative read-then-remove protocol does not provide an
1880
+ atomic compare-and-delete across competing processes.
1881
+
1871
1882
  Replacement removes files absent from the selected runtime content and restores
1872
1883
  the prior tree if commit fails. `signal` and
1873
1884
  `onEvent` remain caller-owned before commit. Once the atomic replacement starts,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.88.0",
3
+ "version": "0.88.1",
4
4
  "description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -98,17 +98,20 @@ async function ensureLockDirectory(workspace){
98
98
  return physicalDirectory(directory,'Workspace operation-lock directory',{create:true});
99
99
  }
100
100
 
101
- function ownerIsAlive(pid){
101
+ function ownerIsAbsent(pid){
102
102
  if(!is.safeInteger(pid)||pid<1)return false;
103
103
  try{
104
104
  process.kill(pid,0);
105
- return true;
105
+ return false;
106
106
  }catch(error){
107
- return error?.code==='EPERM';
107
+ if(error?.code==='ESRCH')return true;
108
+ const busyError=workspaceBusy('The recorded Arcane workspace operation owner could not be determined.');
109
+ busyError.cause=error;
110
+ throw busyError;
108
111
  }
109
112
  }
110
113
 
111
- function lockDocument(value,{workspaceRoot,now=Date.now()}={}){
114
+ function lockDocument(value,{workspaceRoot}={}){
112
115
  if(!value||!is.object(value)||is.array(value)
113
116
  ||value.schemaVersion!==1
114
117
  ||value.kind!=='arcane-workspace-operation-lock'
@@ -124,7 +127,7 @@ function lockDocument(value,{workspaceRoot,now=Date.now()}={}){
124
127
  }
125
128
  const expiresAt=Date.parse(value.expiresAt);
126
129
  if(!is.finite(expiresAt))return null;
127
- return {...value,expired:expiresAt<=now};
130
+ return value;
128
131
  }
129
132
 
130
133
  async function readLock(lockPath,workspaceRoot){
@@ -145,16 +148,24 @@ async function readLock(lockPath,workspaceRoot){
145
148
  return lockDocument(value,{workspaceRoot});
146
149
  }
147
150
 
148
- async function recoverExpiredLock(lockPath,workspaceRoot,onEvent){
151
+ async function recoverAbsentOwnerLock(lockPath,workspaceRoot,onEvent,signal){
152
+ throwIfAborted(signal);
149
153
  const document=await readLock(lockPath,workspaceRoot);
150
- if(!document?.expired||ownerIsAlive(document.owner.pid))return false;
154
+ throwIfAborted(signal);
155
+ if(!document||!ownerIsAbsent(document.owner.pid))return false;
151
156
  await onEvent?.({
152
157
  type:'workspace.operation.stale-recovered',
153
158
  workspaceRoot,
154
159
  lockPath,
155
- previousOwner:document.owner,
160
+ previousOwner:{...document.owner},
156
161
  previousOperation:document.operation
157
162
  });
163
+ throwIfAborted(signal);
164
+ const current=await readLock(lockPath,workspaceRoot);
165
+ throwIfAborted(signal);
166
+ // Observers may replace the record; their completion does not retain ownership.
167
+ if(current?.nonce!==document.nonce||current?.owner.pid!==document.owner.pid
168
+ ||!ownerIsAbsent(current.owner.pid))return false;
158
169
  await rm(lockPath);
159
170
  return true;
160
171
  }
@@ -254,15 +265,17 @@ async function acquire({
254
265
  lockPath=path.join(directory.canonical,LOCK_NAME);
255
266
  let recovered=false;
256
267
  for(;;){
268
+ throwIfAborted(signal);
257
269
  try{
258
270
  handle=await open(lockPath,OPEN_EXCLUSIVE,0o600);
259
271
  break;
260
272
  }catch(error){
261
273
  if(error?.code!=='EEXIST')throw error;
262
- if(recovered||!(await recoverExpiredLock(
274
+ if(recovered||!(await recoverAbsentOwnerLock(
263
275
  lockPath,
264
276
  workspace.canonical,
265
- onEvent
277
+ onEvent,
278
+ signal
266
279
  ))){
267
280
  fail(
268
281
  `Workspace is locked by another Arcane operation: ${lockPath}. `