@awebai/oats 0.22.0 → 0.22.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/README.md +30 -47
- package/bin/oats.mjs +11 -6
- package/capabilities/oats-authoring/LICENSE +21 -0
- package/capabilities/oats-authoring/oats-package.json +11 -0
- package/capabilities/oats-authoring/oats.json +4 -4
- package/capabilities/oats-authoring/skills/integration-authoring/SKILL.md +63 -0
- package/capabilities/oats-authoring/skills/skill-craft/SKILL.md +109 -0
- package/capabilities/oats-authoring/skills/soul-craft/SKILL.md +109 -0
- package/capabilities/oats-aweb/oats.json +6 -6
- package/capabilities/oats-aweb/skills/LICENSE +21 -0
- package/capabilities/oats-aweb/skills/VENDORED.md +26 -0
- package/capabilities/oats-aweb/skills/aweb-identity/SKILL.md +201 -0
- package/capabilities/oats-aweb/skills/aweb-messaging/SKILL.md +161 -0
- package/capabilities/oats-aweb/skills/aweb-messaging/references/messaging-scenarios.md +61 -0
- package/capabilities/oats-aweb/skills/aweb-team-membership/SKILL.md +328 -0
- package/capabilities/oats-aweb/skills/aweb-team-membership/references/team-membership-reference.md +74 -0
- package/capabilities/oats-jira/oats.json +1 -1
- package/capabilities/oats-linear/oats.json +1 -1
- package/capabilities/oats-okf/agents/memory-harvest/soul.yaml +6 -0
- package/capabilities/oats-okf/bin/oats-okf.mjs +82 -51
- package/capabilities/oats-okf/oats.json +4 -1
- package/capabilities/oats-review/oats.json +1 -1
- package/docs/2026-09-03-architecture-proposal.md +642 -0
- package/docs/first-team-demo.md +87 -0
- package/docs/first-team.md +179 -0
- package/docs/integrations.md +83 -65
- package/docs/layers.md +356 -80
- package/docs/migration-from-oas.md +80 -116
- package/docs/release-notes/v0.22.1.md +106 -0
- package/lib/core.mjs +255 -363
- package/package.json +1 -1
- package/packages/record/README.md +76 -16
- package/packages/record/docs/turn-record-sot.md +1 -1
- package/packages/record/lib/store.mjs +207 -43
- package/capabilities/oats-aweb/package.json +0 -20
- package/capabilities/oats-jira/package.json +0 -25
- package/capabilities/oats-linear/README.md +0 -234
- package/capabilities/oats-linear/package.json +0 -29
- package/capabilities/oats-linear/test/oats-linear.test.mjs +0 -168
- package/capabilities/oats-okf/package.json +0 -22
- /package/capabilities/oats-okf/agents/{memory-harvest.md → memory-harvest/AGENTS.md} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awebai/oats",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.1",
|
|
4
4
|
"description": "OATS (Open Agent Team Specification) — durable souls, disposable instances, targetable capability packages, and the runtime-neutral oats CLI/kernel.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -6,12 +6,17 @@ contract is [`docs/turn-record-sot.md`](docs/turn-record-sot.md) in this
|
|
|
6
6
|
package; the conformance vectors under `test/vectors/` pin the format
|
|
7
7
|
(`node test/vectors/validate.mjs`, dependency-free).
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
Turns in this append-only record are content-addressed. Native session turns
|
|
10
|
+
are not signed. Projected aweb mail and chat keep their original message
|
|
11
|
+
signatures verbatim.
|
|
12
|
+
|
|
13
|
+
After `turn-record setup` runs on a machine, this package gathers Claude Code,
|
|
14
|
+
Pi, and Codex transcripts plus aw client logs. It skips sources matched by the
|
|
15
|
+
local record's ignore list. The package keeps captured conversations durably
|
|
16
|
+
and finds them again. The experimental tools that select and synthesize over
|
|
17
|
+
the record (dress, spawn, segments, mind) live in `packages/experimental` of
|
|
18
|
+
the oats repo and run as `oats experimental <cmd>`. They are deliberately not
|
|
19
|
+
part of this package.
|
|
15
20
|
|
|
16
21
|
- **`lib/canonical.mjs`** — canonical JSON (integers only in the core),
|
|
17
22
|
`t1:` content ids, did:key Ed25519 verification. Byte-compatible with awid
|
|
@@ -126,16 +131,71 @@ non-prefix copy of a stream quarantines it loudly.
|
|
|
126
131
|
|
|
127
132
|
## Durability and concurrency
|
|
128
133
|
|
|
129
|
-
Appends and merges run under a per-stream lockfile
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
134
|
+
Appends and merges run under a per-stream lockfile (`streams/<id>/.lock`),
|
|
135
|
+
because the torn-tail repair is a read-truncate-write sequence and hooks,
|
|
136
|
+
watchers and manual passes can fire concurrently for the same owner.
|
|
137
|
+
|
|
138
|
+
The lock carries an **owner token** — the holder's pid, its hostname, and a
|
|
139
|
+
random nonce — written by hard-linking a fully-written temp file into place,
|
|
140
|
+
so a lock that exists is always readable in full. Two guarantees rest on
|
|
141
|
+
that token:
|
|
142
|
+
|
|
143
|
+
- **A live holder is never stolen from, at any age.** Staleness is proven
|
|
144
|
+
from the holder's liveness, not from how long ago the lock was created.
|
|
145
|
+
A contender that finds the holder's pid alive on this host waits, and
|
|
146
|
+
fails with a timeout naming the holder — it does not reclaim. (The
|
|
147
|
+
earlier design judged a lock stale by age while the waiter's timeout
|
|
148
|
+
started when the contender arrived; a contender arriving late therefore
|
|
149
|
+
reclaimed a lock whose holder was still inside its critical section, and
|
|
150
|
+
both ran at once.)
|
|
151
|
+
- **A holder releases only its own lock.** Release unlinks `.lock` only
|
|
152
|
+
while it still carries the releaser's nonce, so a holder whose lock was
|
|
153
|
+
reclaimed cannot delete the replacement lock out from under its new
|
|
154
|
+
owner.
|
|
155
|
+
|
|
156
|
+
**Crash recovery is immediate, not timed**: a lock whose pid is gone from
|
|
157
|
+
this host is provably stale and is reclaimed at once, without waiting out
|
|
158
|
+
any threshold.
|
|
159
|
+
|
|
160
|
+
A lock that cannot be read at all (anything but "not there" — a permission
|
|
161
|
+
error, a directory in its place) is not retried: acquisition fails at once
|
|
162
|
+
with the underlying error code and the lock's path, because retrying cannot
|
|
163
|
+
clear such a condition. Everything else that fails to acquire — including a
|
|
164
|
+
lock judged stale whose removal keeps failing — is bounded by
|
|
165
|
+
`lockTimeoutMs` and reports why on timeout.
|
|
166
|
+
|
|
167
|
+
`lockStaleMs` (30 s by default) is the fallback for the two cases where
|
|
168
|
+
liveness cannot be checked here: a lock created on **another host** (file
|
|
169
|
+
sync can copy one in) and a lock with **no readable token** (written by an
|
|
170
|
+
older version of this package, or by hand). Those are still reclaimed by
|
|
171
|
+
age. `lockTimeoutMs` (10 s) is how long a contender waits; the two
|
|
172
|
+
thresholds are independent, and neither ordering of them is required or
|
|
173
|
+
assumed.
|
|
174
|
+
|
|
175
|
+
Honest limits:
|
|
176
|
+
|
|
177
|
+
- **Pid reuse across a reboot.** A lock left by a process whose pid was
|
|
178
|
+
later reused by an unrelated live process reads as held forever, and
|
|
179
|
+
contenders on that stream time out until it is deleted by hand. The
|
|
180
|
+
timeout message names the lock path and the recorded holder. This is the
|
|
181
|
+
chosen direction of failure: refusing to write is recoverable, writing
|
|
182
|
+
concurrently with a live holder is not.
|
|
183
|
+
- **Reclaim is not perfectly atomic.** A contender re-checks the lock's
|
|
184
|
+
identity (inode and mtime) immediately before removing it. That recheck
|
|
185
|
+
**detects** a replacement that landed while the old lock was being proven
|
|
186
|
+
stale — the common cascade, one contender reclaiming from whoever
|
|
187
|
+
reclaimed first — and the contender gives up and retries. It is a
|
|
188
|
+
detection, not an exclusion: a replacement landing in the window between
|
|
189
|
+
the recheck and the unlink is not seen and is removed. Nothing here
|
|
190
|
+
defends against that window.
|
|
191
|
+
- **Sync tools may copy a `.lock` file.** That cannot corrupt data, but a
|
|
192
|
+
synced-in lock from another host is judged by age, so it can delay a local
|
|
193
|
+
`mergeStreamCopy` on that stream by up to 30 s — excluding `.lock` from
|
|
194
|
+
sync patterns remains the right configuration.
|
|
195
|
+
|
|
196
|
+
Journal writes fsync; note that on macOS `fsync(2)` does not guarantee media
|
|
197
|
+
durability (that would need `F_FULLFSYNC`, which Node's fs API does not
|
|
198
|
+
expose) — the guarantee is OS-crash-level, not power-loss-level.
|
|
139
199
|
|
|
140
200
|
## Upgrading
|
|
141
201
|
|
|
@@ -8,7 +8,7 @@ aweb mail and chat messages into it. It is the contract implemented by the
|
|
|
8
8
|
record tools (`capture`, `recall`, and the experimental tools over them); this
|
|
9
9
|
package (`@awebai/turn-record`, in the oats repo) is the reference
|
|
10
10
|
implementation, and the spec lives beside it on purpose: the record spans
|
|
11
|
-
agent sessions from
|
|
11
|
+
agent sessions from supported harnesses, and the aweb server is one projected
|
|
12
12
|
source, not the record's home. The architecture decision it implements is
|
|
13
13
|
`2026-08-18-turn-record-and-tools.md` in the strategy repo
|
|
14
14
|
(github.com/awebai/strategy); the identity and messaging contracts it builds
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
existsSync,
|
|
20
20
|
fsyncSync,
|
|
21
21
|
ftruncateSync,
|
|
22
|
+
linkSync,
|
|
22
23
|
mkdirSync,
|
|
23
24
|
openSync,
|
|
24
25
|
readdirSync,
|
|
@@ -28,8 +29,9 @@ import {
|
|
|
28
29
|
statSync,
|
|
29
30
|
unlinkSync,
|
|
30
31
|
writeFileSync,
|
|
31
|
-
writeSync,
|
|
32
32
|
} from "node:fs";
|
|
33
|
+
import { randomBytes } from "node:crypto";
|
|
34
|
+
import { hostname } from "node:os";
|
|
33
35
|
import { join } from "node:path";
|
|
34
36
|
|
|
35
37
|
import { finishTurn, sha256Hex, verifyTurnId } from "./canonical.mjs";
|
|
@@ -141,16 +143,186 @@ export function parseJournal(input) {
|
|
|
141
143
|
return { turns, validEnd, torn: validEnd < bytes.length };
|
|
142
144
|
}
|
|
143
145
|
|
|
146
|
+
// ----------------------------------------------------------- stream lock
|
|
147
|
+
//
|
|
148
|
+
// The per-stream lock is an exclusive-create lockfile carrying an OWNER
|
|
149
|
+
// TOKEN: the creating process's pid, its hostname, and a random nonce.
|
|
150
|
+
// Two properties follow from the token that a bare lockfile cannot give:
|
|
151
|
+
//
|
|
152
|
+
// - staleness is judged against the HOLDER'S LIVENESS, not the lock's
|
|
153
|
+
// age, so a contender that arrives late against a still-running holder
|
|
154
|
+
// waits (or times out) instead of stealing a live lock; and
|
|
155
|
+
// - release unlinks the lock only while it still carries the releaser's
|
|
156
|
+
// nonce, so a holder whose lock was reclaimed cannot delete the
|
|
157
|
+
// replacement lock out from under its new owner.
|
|
158
|
+
//
|
|
159
|
+
// Crash recovery is preserved: a lock whose pid is gone from this host is
|
|
160
|
+
// provably stale and is reclaimed at once, without waiting out any age.
|
|
161
|
+
//
|
|
162
|
+
// Age remains the fallback for the two cases where liveness is not
|
|
163
|
+
// knowable here: a lock created on another host (file sync can copy one
|
|
164
|
+
// in) and a lock with no readable token (written by an older version, or
|
|
165
|
+
// by hand).
|
|
166
|
+
|
|
167
|
+
function newLockToken() {
|
|
168
|
+
return {
|
|
169
|
+
pid: process.pid,
|
|
170
|
+
host: hostname(),
|
|
171
|
+
nonce: randomBytes(12).toString("hex"),
|
|
172
|
+
acquiredAt: new Date().toISOString(),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Create the lock, or report that someone else holds it. The payload is
|
|
177
|
+
// written to a temp file and hard-linked into place, so the lock never
|
|
178
|
+
// exists with partial or empty content: a contender that can see the file
|
|
179
|
+
// can always read the whole token.
|
|
180
|
+
function createLock(lockPath, dir, token) {
|
|
181
|
+
const tmp = join(dir, `.lock.tmp-${token.pid}-${token.nonce}`);
|
|
182
|
+
try {
|
|
183
|
+
writeFileSync(tmp, JSON.stringify(token) + "\n");
|
|
184
|
+
try {
|
|
185
|
+
linkSync(tmp, lockPath);
|
|
186
|
+
return true;
|
|
187
|
+
} catch (err) {
|
|
188
|
+
if (err.code === "EEXIST") return false;
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
191
|
+
} finally {
|
|
192
|
+
try {
|
|
193
|
+
unlinkSync(tmp);
|
|
194
|
+
} catch {
|
|
195
|
+
/* never created, or already gone */
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Read the lock as { token, ino, mtimeMs }; token is null when the file
|
|
201
|
+
// holds no readable owner token. Returns null when the lock has vanished.
|
|
202
|
+
function readLock(lockPath) {
|
|
203
|
+
let st;
|
|
204
|
+
let raw;
|
|
205
|
+
try {
|
|
206
|
+
st = statSync(lockPath);
|
|
207
|
+
raw = readFileSync(lockPath, "utf8");
|
|
208
|
+
} catch (err) {
|
|
209
|
+
if (err.code === "ENOENT") return null; // vanished: the holder released
|
|
210
|
+
// Anything else (EACCES, EISDIR, EIO...) is a condition retrying cannot
|
|
211
|
+
// clear. Swallowing it here is what let the acquire loop spin: it read
|
|
212
|
+
// as "vanished" forever. Fail with the underlying code and path so the
|
|
213
|
+
// operator can act on it.
|
|
214
|
+
throw new StoreError(
|
|
215
|
+
`cannot read stream lock ${lockPath}: ${err.code ?? "unknown error"} (${err.message})`,
|
|
216
|
+
{ cause: err },
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
let token = null;
|
|
220
|
+
try {
|
|
221
|
+
const parsed = JSON.parse(raw);
|
|
222
|
+
if (
|
|
223
|
+
parsed &&
|
|
224
|
+
typeof parsed.pid === "number" &&
|
|
225
|
+
typeof parsed.host === "string" &&
|
|
226
|
+
typeof parsed.nonce === "string"
|
|
227
|
+
) {
|
|
228
|
+
token = parsed;
|
|
229
|
+
}
|
|
230
|
+
} catch {
|
|
231
|
+
/* no token: an older or hand-written lock, judged by age below */
|
|
232
|
+
}
|
|
233
|
+
return { token, ino: st.ino, mtimeMs: st.mtimeMs };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// true / false when the holder's liveness is knowable from this process,
|
|
237
|
+
// null when it is not (the lock was created on another host). EPERM means
|
|
238
|
+
// the process exists and belongs to another user: alive.
|
|
239
|
+
function holderAlive(token) {
|
|
240
|
+
if (token.host !== hostname()) return null;
|
|
241
|
+
try {
|
|
242
|
+
process.kill(token.pid, 0);
|
|
243
|
+
return true;
|
|
244
|
+
} catch (err) {
|
|
245
|
+
return err.code === "ESRCH" ? false : true;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function lockIsProvenStale(held, staleMs) {
|
|
250
|
+
if (held.token) {
|
|
251
|
+
const alive = holderAlive(held.token);
|
|
252
|
+
if (alive === true) return false; // live holder: never stealable, at any age
|
|
253
|
+
if (alive === false) return true; // dead holder: crash recovery
|
|
254
|
+
}
|
|
255
|
+
return Date.now() - held.mtimeMs > staleMs;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Remove a lock we have proven stale — and only that lock. Re-checking the
|
|
259
|
+
// file's identity immediately before the unlink DETECTS a replacement that
|
|
260
|
+
// landed while we were proving the old lock stale, so the common cascade
|
|
261
|
+
// (reclaiming from whoever reclaimed first) is caught. It is a detection,
|
|
262
|
+
// not an exclusion: a replacement landing between this recheck and the
|
|
263
|
+
// unlink is still removed, and nothing here defends against that window.
|
|
264
|
+
//
|
|
265
|
+
// Returns null when there is nothing to report — the lock was removed, was
|
|
266
|
+
// already gone, or had been replaced since we proved it stale; all three
|
|
267
|
+
// mean "retry". Returns the underlying error when the removal itself keeps
|
|
268
|
+
// failing (a read-only or hostile directory), which the caller reports on
|
|
269
|
+
// timeout rather than retrying silently forever.
|
|
270
|
+
function removeStaleLock(lockPath, held) {
|
|
271
|
+
let st;
|
|
272
|
+
try {
|
|
273
|
+
st = statSync(lockPath);
|
|
274
|
+
} catch (err) {
|
|
275
|
+
return err.code === "ENOENT" ? null : err; // already gone
|
|
276
|
+
}
|
|
277
|
+
if (st.ino !== held.ino || st.mtimeMs !== held.mtimeMs) return null; // replaced
|
|
278
|
+
try {
|
|
279
|
+
unlinkSync(lockPath);
|
|
280
|
+
return null;
|
|
281
|
+
} catch (err) {
|
|
282
|
+
return err.code === "ENOENT" ? null : err; // beaten to it
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Release: unlink only while the lock still carries our nonce. If it was
|
|
287
|
+
// reclaimed (we ran long enough to be judged dead, or a sync overwrote
|
|
288
|
+
// it), the file now belongs to another holder and is left alone.
|
|
289
|
+
function releaseOwnedLock(lockPath, token) {
|
|
290
|
+
// Release runs in a finally block: it must not replace the critical
|
|
291
|
+
// section's own outcome with a lock error. A lock we cannot read is also
|
|
292
|
+
// a lock whose ownership we cannot prove, so leaving it is the correct
|
|
293
|
+
// move as well as the safe one — it is reclaimable by liveness once this
|
|
294
|
+
// process exits.
|
|
295
|
+
let held;
|
|
296
|
+
try {
|
|
297
|
+
held = readLock(lockPath);
|
|
298
|
+
} catch {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (held === null) return;
|
|
302
|
+
if (!held.token || held.token.nonce !== token.nonce) return;
|
|
303
|
+
try {
|
|
304
|
+
unlinkSync(lockPath);
|
|
305
|
+
} catch {
|
|
306
|
+
/* already gone */
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function describeHolder(held) {
|
|
311
|
+
if (!held || !held.token) return "holder unknown";
|
|
312
|
+
return `held by pid ${held.token.pid} on ${held.token.host} since ${held.token.acquiredAt}`;
|
|
313
|
+
}
|
|
314
|
+
|
|
144
315
|
export class RecordStore {
|
|
145
316
|
constructor(root, { owner, lockTimeoutMs = 10000, lockStaleMs = 30000 } = {}) {
|
|
146
317
|
if (!root) throw new StoreError("record root is required");
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
318
|
+
// lockTimeoutMs is how long a contender waits; lockStaleMs is how old a
|
|
319
|
+
// lock must be before it is reclaimed WHEN ITS HOLDER'S LIVENESS CANNOT
|
|
320
|
+
// BE CHECKED (foreign host, no readable token). There is deliberately no
|
|
321
|
+
// ordering constraint between them: safety against stealing a live
|
|
322
|
+
// holder's lock comes from the owner token, not from the two thresholds
|
|
323
|
+
// being ordered — an ordering that never held, because the waiter's
|
|
324
|
+
// timeout starts when the contender arrives while age is measured from
|
|
325
|
+
// the lock's creation.
|
|
154
326
|
this.root = root;
|
|
155
327
|
this.owner = owner ?? null;
|
|
156
328
|
this.lockTimeoutMs = lockTimeoutMs;
|
|
@@ -166,52 +338,44 @@ export class RecordStore {
|
|
|
166
338
|
// processes: hooks, watchers and manual passes can all fire close
|
|
167
339
|
// together for the same owner, and an unguarded repair can truncate a
|
|
168
340
|
// concurrent writer's already-fsynced turn. Exclusive-create lockfile
|
|
169
|
-
//
|
|
170
|
-
//
|
|
341
|
+
// carrying an owner token; see the stream-lock helpers above for how
|
|
342
|
+
// staleness is proven and why release is ownership-checked.
|
|
171
343
|
withStreamLock(streamId, fn) {
|
|
172
344
|
const dir = join(this.streamsDir(), streamId);
|
|
173
345
|
mkdirSync(dir, { recursive: true });
|
|
174
346
|
const lockPath = join(dir, ".lock");
|
|
347
|
+
const token = newLockToken();
|
|
175
348
|
const deadline = Date.now() + this.lockTimeoutMs;
|
|
349
|
+
// Every iteration that fails to acquire falls through to ONE deadline
|
|
350
|
+
// check and one sleep — there is deliberately no `continue` above them,
|
|
351
|
+
// and no branch that retries "for free". An earlier version checked the
|
|
352
|
+
// deadline only on the readable-and-live branch, so a lock that kept
|
|
353
|
+
// failing to be read, or kept failing to be removed, or kept reading as
|
|
354
|
+
// vanished (a dangling symlink does exactly that) retried forever, hot,
|
|
355
|
+
// with nothing able to stop it. Retrying at once after a reclaim would
|
|
356
|
+
// buy ~25ms on the crash-recovery path and reopen that hole; it is not
|
|
357
|
+
// worth it.
|
|
176
358
|
for (;;) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
} catch {
|
|
191
|
-
continue; // lock vanished between open and stat: retry now
|
|
192
|
-
}
|
|
193
|
-
if (stale) {
|
|
194
|
-
try {
|
|
195
|
-
unlinkSync(lockPath);
|
|
196
|
-
} catch {
|
|
197
|
-
/* another process stole it first */
|
|
198
|
-
}
|
|
199
|
-
continue;
|
|
200
|
-
}
|
|
201
|
-
if (Date.now() >= deadline) {
|
|
202
|
-
throw new StoreError(`timed out waiting for lock on stream ${streamId} (${lockPath})`);
|
|
203
|
-
}
|
|
204
|
-
sleepSync(25);
|
|
359
|
+
if (createLock(lockPath, dir, token)) break;
|
|
360
|
+
const held = readLock(lockPath); // throws on anything but ENOENT
|
|
361
|
+
const reclaimError =
|
|
362
|
+
held !== null && lockIsProvenStale(held, this.lockStaleMs)
|
|
363
|
+
? removeStaleLock(lockPath, held)
|
|
364
|
+
: null;
|
|
365
|
+
if (Date.now() >= deadline) {
|
|
366
|
+
throw new StoreError(
|
|
367
|
+
`timed out waiting for lock on stream ${streamId} (${lockPath}, ${describeHolder(held)})` +
|
|
368
|
+
(reclaimError
|
|
369
|
+
? `; it was judged stale but could not be removed: ${reclaimError.code ?? "unknown error"} (${reclaimError.message})`
|
|
370
|
+
: ""),
|
|
371
|
+
);
|
|
205
372
|
}
|
|
373
|
+
sleepSync(25);
|
|
206
374
|
}
|
|
207
375
|
try {
|
|
208
376
|
return fn();
|
|
209
377
|
} finally {
|
|
210
|
-
|
|
211
|
-
unlinkSync(lockPath);
|
|
212
|
-
} catch {
|
|
213
|
-
/* already stolen as stale; nothing to release */
|
|
214
|
-
}
|
|
378
|
+
releaseOwnedLock(lockPath, token);
|
|
215
379
|
}
|
|
216
380
|
}
|
|
217
381
|
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@awebai/oats-aweb",
|
|
3
|
-
"version": "0.2.0",
|
|
4
|
-
"description": "OATS messaging provider: per-instance aweb identities (mint at spawn, self-delete at retire), cross-machine roster, guided setup, and agents-md injection",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"pi-package",
|
|
7
|
-
"oats",
|
|
8
|
-
"aweb"
|
|
9
|
-
],
|
|
10
|
-
"repository": {
|
|
11
|
-
"type": "git",
|
|
12
|
-
"url": "https://github.com/awebai/oats",
|
|
13
|
-
"directory": "capabilities/oats-aweb"
|
|
14
|
-
},
|
|
15
|
-
"license": "MIT",
|
|
16
|
-
"type": "module",
|
|
17
|
-
"dependencies": {
|
|
18
|
-
"@awebai/pi": "^0.2.1"
|
|
19
|
-
}
|
|
20
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@awebai/oats-jira",
|
|
3
|
-
"version": "0.1.0",
|
|
4
|
-
"description": "OATS tasks provider: Jira via acli — epic/story/task protocol, agent roster, label-based agent identity, and agents-md injection",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"pi-package",
|
|
7
|
-
"oats",
|
|
8
|
-
"jira"
|
|
9
|
-
],
|
|
10
|
-
"repository": {
|
|
11
|
-
"type": "git",
|
|
12
|
-
"url": "https://github.com/awebai/oats",
|
|
13
|
-
"directory": "capabilities/oats-jira"
|
|
14
|
-
},
|
|
15
|
-
"license": "MIT",
|
|
16
|
-
"type": "module",
|
|
17
|
-
"bin": {
|
|
18
|
-
"oats-jira": "./bin/oats-jira.mjs"
|
|
19
|
-
},
|
|
20
|
-
"pi": {
|
|
21
|
-
"skills": [
|
|
22
|
-
"./skills"
|
|
23
|
-
]
|
|
24
|
-
}
|
|
25
|
-
}
|