@raidiant/notifai 10.1.0 → 10.1.2
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 +37 -0
- package/README.md +3 -0
- package/dist/cli-bin.d.ts +21 -1
- package/dist/cli-bin.js +141 -10
- package/dist/cli-bin.js.map +1 -1
- package/dist/cli-update-contract.d.ts +7 -0
- package/dist/cli-update-contract.js +10 -0
- package/dist/cli-update-contract.js.map +1 -0
- package/dist/client.d.ts +6 -1
- package/dist/client.js +1 -0
- package/dist/client.js.map +1 -1
- package/dist/codex-wake.d.ts +2 -0
- package/dist/codex-wake.js +2 -1
- package/dist/codex-wake.js.map +1 -1
- package/dist/commands-auth.d.ts +15 -1
- package/dist/commands-auth.js +29 -11
- package/dist/commands-auth.js.map +1 -1
- package/dist/commands-core.d.ts +9 -2
- package/dist/commands-core.js +11 -4
- package/dist/commands-core.js.map +1 -1
- package/dist/commands-devices.d.ts +10 -1
- package/dist/commands-devices.js +17 -13
- package/dist/commands-devices.js.map +1 -1
- package/dist/commands-doctor.js +93 -49
- package/dist/commands-doctor.js.map +1 -1
- package/dist/commands-hooks.js +47 -10
- package/dist/commands-hooks.js.map +1 -1
- package/dist/commands-init.d.ts +16 -0
- package/dist/commands-init.js +254 -93
- package/dist/commands-init.js.map +1 -1
- package/dist/commands-send-support.js +3 -0
- package/dist/commands-send-support.js.map +1 -1
- package/dist/commands-send.d.ts +1 -1
- package/dist/commands-send.js +121 -35
- package/dist/commands-send.js.map +1 -1
- package/dist/commands-skill.js +72 -54
- package/dist/commands-skill.js.map +1 -1
- package/dist/commands-update.d.ts +10 -0
- package/dist/commands-update.js +141 -0
- package/dist/commands-update.js.map +1 -0
- package/dist/commands.d.ts +2 -1
- package/dist/commands.js +2 -1
- package/dist/commands.js.map +1 -1
- package/dist/hook-adapter.d.ts +2 -0
- package/dist/hook-adapter.js +12 -1
- package/dist/hook-adapter.js.map +1 -1
- package/dist/hooks.d.ts +43 -2
- package/dist/hooks.js +163 -21
- package/dist/hooks.js.map +1 -1
- package/dist/invocation-context.d.ts +1 -0
- package/dist/invocation-context.js +9 -1
- package/dist/invocation-context.js.map +1 -1
- package/dist/main-run.d.ts +1 -0
- package/dist/main-run.js +42 -0
- package/dist/main-run.js.map +1 -0
- package/dist/main.js +10 -31
- package/dist/main.js.map +1 -1
- package/dist/native-skills.d.ts +13 -3
- package/dist/native-skills.js +51 -12
- package/dist/native-skills.js.map +1 -1
- package/dist/node-floor.d.ts +19 -0
- package/dist/node-floor.js +39 -0
- package/dist/node-floor.js.map +1 -0
- package/dist/orca-session-title.d.ts +5 -4
- package/dist/orca-session-title.js +32 -19
- package/dist/orca-session-title.js.map +1 -1
- package/dist/platform.d.ts +11 -17
- package/dist/platform.js +20 -23
- package/dist/platform.js.map +1 -1
- package/dist/program.d.ts +2 -1
- package/dist/program.js +22 -5
- package/dist/program.js.map +1 -1
- package/dist/question-settlement-process.d.ts +15 -0
- package/dist/question-settlement-process.js +35 -0
- package/dist/question-settlement-process.js.map +1 -0
- package/dist/readiness.d.ts +25 -1
- package/dist/readiness.js +32 -2
- package/dist/readiness.js.map +1 -1
- package/dist/release.d.ts +5 -5
- package/dist/release.js +5 -5
- package/dist/setup-destinations.d.ts +23 -0
- package/dist/setup-destinations.js +44 -0
- package/dist/setup-destinations.js.map +1 -0
- package/dist/skill-integrity.d.ts +49 -0
- package/dist/skill-integrity.js +154 -0
- package/dist/skill-integrity.js.map +1 -0
- package/dist/skill-source/manifest.json +16 -0
- package/dist/skill-source/notifai/SKILL.md +390 -0
- package/dist/skill-source/notifai/references/harness-setup.md +220 -0
- package/package.json +11 -14
- package/src/atomic-file.ts +0 -198
- package/src/claude-wake.test.ts +0 -321
- package/src/claude-wake.ts +0 -499
- package/src/cli-bin.test.ts +0 -38
- package/src/cli-bin.ts +0 -67
- package/src/cli-release.test.ts +0 -46
- package/src/cli-release.ts +0 -78
- package/src/client.test.ts +0 -390
- package/src/client.ts +0 -345
- package/src/codex-wake.test.ts +0 -254
- package/src/codex-wake.ts +0 -297
- package/src/command-session.ts +0 -38
- package/src/commands-agent-sessions.ts +0 -72
- package/src/commands-auth.ts +0 -205
- package/src/commands-config.ts +0 -336
- package/src/commands-core.ts +0 -298
- package/src/commands-devices.test.ts +0 -15
- package/src/commands-devices.ts +0 -173
- package/src/commands-doctor.ts +0 -1440
- package/src/commands-guidance.ts +0 -181
- package/src/commands-harness-context.ts +0 -207
- package/src/commands-hook-shape.ts +0 -69
- package/src/commands-hooks.ts +0 -2268
- package/src/commands-init.ts +0 -878
- package/src/commands-io.ts +0 -108
- package/src/commands-logs.ts +0 -243
- package/src/commands-project.ts +0 -47
- package/src/commands-send-support.test.ts +0 -156
- package/src/commands-send-support.ts +0 -311
- package/src/commands-send.ts +0 -769
- package/src/commands-setup-proof.test.ts +0 -72
- package/src/commands-setup-proof.ts +0 -124
- package/src/commands-skill.test.ts +0 -44
- package/src/commands-skill.ts +0 -204
- package/src/commands-sounds.test.ts +0 -106
- package/src/commands-sounds.ts +0 -49
- package/src/commands.test.ts +0 -7828
- package/src/commands.ts +0 -75
- package/src/config-schema.test.ts +0 -153
- package/src/config-schema.ts +0 -349
- package/src/config.test.ts +0 -581
- package/src/config.ts +0 -475
- package/src/credentials.test.ts +0 -403
- package/src/credentials.ts +0 -403
- package/src/file-lock-process-worker.test.ts +0 -64
- package/src/file-lock.test.ts +0 -320
- package/src/file-lock.ts +0 -393
- package/src/fixtures/hermes-0.20.6-classic-cli-local.json +0 -50
- package/src/fixtures/openclaw-2026.7.1-2-agent-local.json +0 -57
- package/src/generated-session-label.ts +0 -155
- package/src/guidance-content.ts +0 -278
- package/src/guidance-render.ts +0 -56
- package/src/guidance.test.ts +0 -327
- package/src/guidance.ts +0 -233
- package/src/harness-integration.test.ts +0 -294
- package/src/harnesses.test.ts +0 -149
- package/src/harnesses.ts +0 -164
- package/src/hook-adapter.test.ts +0 -496
- package/src/hook-adapter.ts +0 -410
- package/src/hook-input.test.ts +0 -39
- package/src/hook-input.ts +0 -72
- package/src/hooks.test.ts +0 -5549
- package/src/hooks.ts +0 -3759
- package/src/install-hooks.test.ts +0 -1614
- package/src/install-hooks.ts +0 -1501
- package/src/interactive.test.ts +0 -387
- package/src/interactive.ts +0 -858
- package/src/invocation-context.test.ts +0 -352
- package/src/invocation-context.ts +0 -197
- package/src/logging-process-worker.test.ts +0 -45
- package/src/logging.test.ts +0 -544
- package/src/logging.ts +0 -698
- package/src/main.ts +0 -36
- package/src/native-skills.test.ts +0 -115
- package/src/native-skills.ts +0 -162
- package/src/onboarding-docs.test.ts +0 -25
- package/src/openclaw-plugin.ts +0 -409
- package/src/opencode-plugin.ts +0 -298
- package/src/orca-session-title.test.ts +0 -54
- package/src/orca-session-title.ts +0 -103
- package/src/pack-feedback-slice.test.ts +0 -212
- package/src/pack-feedback-slice.ts +0 -187
- package/src/platform.test.ts +0 -152
- package/src/platform.ts +0 -173
- package/src/program.test.ts +0 -282
- package/src/program.ts +0 -818
- package/src/project-enablement.test.ts +0 -55
- package/src/project-enablement.ts +0 -99
- package/src/question-caps.test.ts +0 -49
- package/src/question-timing.ts +0 -56
- package/src/readiness.test.ts +0 -25
- package/src/readiness.ts +0 -179
- package/src/release.ts +0 -51
- package/src/send-attempts.test.ts +0 -143
- package/src/send-attempts.ts +0 -188
- package/src/send.test.ts +0 -28
- package/src/send.ts +0 -436
- package/src/session-activation.ts +0 -61
- package/src/session-end-log-lock-worker.test.ts +0 -35
- package/src/session-labels.test.ts +0 -490
- package/src/session-labels.ts +0 -474
- package/src/skill.test.ts +0 -415
- package/src/sound-ref.test.ts +0 -31
- package/src/sound-ref.ts +0 -34
- package/src/trust-policy.test.ts +0 -701
- package/src/ui/banner.ts +0 -95
- package/src/ui/config-view.test.ts +0 -90
- package/src/ui/config-view.ts +0 -138
- package/src/ui/help.test.ts +0 -16
- package/src/ui/help.ts +0 -128
- package/src/ui/theme.test.ts +0 -92
- package/src/ui/theme.ts +0 -265
- package/src/url-policy.ts +0 -399
- package/tsconfig.json +0 -10
package/src/file-lock.ts
DELETED
|
@@ -1,393 +0,0 @@
|
|
|
1
|
-
import { randomBytes } from 'node:crypto'
|
|
2
|
-
import {
|
|
3
|
-
chmodSync,
|
|
4
|
-
closeSync,
|
|
5
|
-
fstatSync,
|
|
6
|
-
lstatSync,
|
|
7
|
-
mkdirSync,
|
|
8
|
-
openSync,
|
|
9
|
-
readdirSync,
|
|
10
|
-
renameSync,
|
|
11
|
-
rmdirSync,
|
|
12
|
-
unlinkSync,
|
|
13
|
-
} from 'node:fs'
|
|
14
|
-
import path from 'node:path'
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Serialize a short cross-process filesystem transaction.
|
|
18
|
-
*
|
|
19
|
-
* Each contender publishes all of its ownership in one unique filename. A
|
|
20
|
-
* two-phase bakery lock then orders live contenders by ticket and process
|
|
21
|
-
* identity. Because paths are never reused, recovering an entry left by a dead
|
|
22
|
-
* process cannot remove a later owner's entry; because process liveness, rather
|
|
23
|
-
* than age, decides recovery, a delayed holder is never mistaken for a dead one.
|
|
24
|
-
*
|
|
25
|
-
* The rendezvous directory lives only as long as it has entries: the last
|
|
26
|
-
* contender out removes it, so an idle machine keeps no lock state. That makes
|
|
27
|
-
* the directory itself a contended resource. A contender registering while
|
|
28
|
-
* another releases can find it gone, half-deleted, or already replaced by a
|
|
29
|
-
* third contender, and none of those is a broken lock — so publication retries
|
|
30
|
-
* within the caller's deadline and pins its rendezvous only once its own entry
|
|
31
|
-
* is in the directory, which is the moment removal becomes impossible.
|
|
32
|
-
*
|
|
33
|
-
* Acquisition is deliberately bounded because both harness hooks and logging
|
|
34
|
-
* must fail open rather than hold an agent turn indefinitely.
|
|
35
|
-
*/
|
|
36
|
-
const FILE_LOCK_WAIT_MS = 1_000
|
|
37
|
-
const FILE_LOCK_POLL_MS = 5
|
|
38
|
-
const FILE_LOCK_TOKEN_BYTES = 12
|
|
39
|
-
const ENTRY_PATTERN = /^(choosing|ticket-(\d+))-(\d+)-([0-9a-f]+)$/
|
|
40
|
-
const lockSleep = new Int32Array(new SharedArrayBuffer(4))
|
|
41
|
-
|
|
42
|
-
export type FileLockObservation =
|
|
43
|
-
/** The rendezvous has been scanned and this contender is about to publish. */
|
|
44
|
-
| { phase: 'registering'; entry: string }
|
|
45
|
-
| { phase: 'choosing-published'; entry: string }
|
|
46
|
-
| { phase: 'stale-entry'; entry: string }
|
|
47
|
-
| { phase: 'waiting'; blockers: string[] }
|
|
48
|
-
|
|
49
|
-
interface FileLockOptions {
|
|
50
|
-
waitMs?: number
|
|
51
|
-
observe?: (observation: FileLockObservation) => void
|
|
52
|
-
/** Installer transactions report lock replacement after a successful action. */
|
|
53
|
-
strictRelease?: boolean
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
interface FileIdentity {
|
|
57
|
-
dev: number
|
|
58
|
-
ino: number
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Identity for one published lock entry.
|
|
63
|
-
*
|
|
64
|
-
* Linux can immediately reuse an inode after an unlinked entry is replaced at
|
|
65
|
-
* the same path. The creation/change timestamp keeps that replacement distinct
|
|
66
|
-
* even when its device and inode numbers happen to match the removed entry.
|
|
67
|
-
*/
|
|
68
|
-
interface EntryIdentity {
|
|
69
|
-
dev: bigint
|
|
70
|
-
ino: bigint
|
|
71
|
-
ctimeNs: bigint
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
interface ChoosingEntry {
|
|
75
|
-
kind: 'choosing'
|
|
76
|
-
name: string
|
|
77
|
-
pid: number
|
|
78
|
-
token: string
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
interface TicketEntry {
|
|
82
|
-
kind: 'ticket'
|
|
83
|
-
name: string
|
|
84
|
-
pid: number
|
|
85
|
-
token: string
|
|
86
|
-
ticket: bigint
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
type LockEntry = ChoosingEntry | TicketEntry
|
|
90
|
-
|
|
91
|
-
function parseEntry(name: string): LockEntry | null {
|
|
92
|
-
const match = ENTRY_PATTERN.exec(name)
|
|
93
|
-
if (match === null) return null
|
|
94
|
-
const pid = Number(match[3])
|
|
95
|
-
if (!Number.isSafeInteger(pid) || pid <= 0) return null
|
|
96
|
-
const token = match[4]!
|
|
97
|
-
if (match[1] === 'choosing') return { kind: 'choosing', name, pid, token }
|
|
98
|
-
try {
|
|
99
|
-
const ticket = BigInt(match[2]!)
|
|
100
|
-
if (ticket <= 0n) return null
|
|
101
|
-
return { kind: 'ticket', name, pid, token, ticket }
|
|
102
|
-
} catch {
|
|
103
|
-
return null
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function processIsAlive(pid: number): boolean {
|
|
108
|
-
if (pid === process.pid) return true
|
|
109
|
-
try {
|
|
110
|
-
process.kill(pid, 0)
|
|
111
|
-
return true
|
|
112
|
-
} catch (err) {
|
|
113
|
-
const code = (err as NodeJS.ErrnoException).code
|
|
114
|
-
if (code === 'ESRCH') return false
|
|
115
|
-
// EPERM means the process exists but is owned by somebody else. Unknown
|
|
116
|
-
// failures are also treated as live: losing recovery is safer than allowing
|
|
117
|
-
// two holders into the transaction.
|
|
118
|
-
return true
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function currentUid(): number | undefined {
|
|
123
|
-
return typeof process.getuid === 'function' ? process.getuid() : undefined
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function assertOwnedDirectory(directory: string): FileIdentity {
|
|
127
|
-
const stat = lstatSync(directory)
|
|
128
|
-
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
129
|
-
throw new Error(`${directory} is not a regular directory; refusing to use it as a file lock.`)
|
|
130
|
-
}
|
|
131
|
-
const uid = currentUid()
|
|
132
|
-
if (uid !== undefined && stat.uid !== uid) {
|
|
133
|
-
throw new Error(`${directory} is owned by uid ${stat.uid}, not the current user.`)
|
|
134
|
-
}
|
|
135
|
-
return { dev: stat.dev, ino: stat.ino }
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* The rendezvous a contender was using is no longer the directory at that path.
|
|
140
|
-
*
|
|
141
|
-
* Fatal once an entry is published — a contender's own entry makes the
|
|
142
|
-
* directory non-empty, so no cooperating releaser can remove it. Before
|
|
143
|
-
* publication it means only that a releaser removed an empty rendezvous while
|
|
144
|
-
* this contender was registering in it, which is ordinary contention.
|
|
145
|
-
*/
|
|
146
|
-
class RendezvousReplaced extends Error {}
|
|
147
|
-
|
|
148
|
-
function assertSameDirectory(directory: string, expected: FileIdentity): void {
|
|
149
|
-
const current = lstatSync(directory)
|
|
150
|
-
if (
|
|
151
|
-
current.isSymbolicLink() ||
|
|
152
|
-
!current.isDirectory() ||
|
|
153
|
-
current.dev !== expected.dev ||
|
|
154
|
-
current.ino !== expected.ino
|
|
155
|
-
) {
|
|
156
|
-
throw new RendezvousReplaced(`${directory} changed while the file lock was active.`)
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function entryIdentity(stat: { dev: bigint; ino: bigint; ctimeNs: bigint }): EntryIdentity {
|
|
161
|
-
return {
|
|
162
|
-
dev: stat.dev,
|
|
163
|
-
ino: stat.ino,
|
|
164
|
-
ctimeNs: stat.ctimeNs,
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function sameEntryIdentity(file: string, expected: EntryIdentity): boolean {
|
|
169
|
-
try {
|
|
170
|
-
const current = lstatSync(file, { bigint: true })
|
|
171
|
-
return (
|
|
172
|
-
current.dev === expected.dev &&
|
|
173
|
-
current.ino === expected.ino &&
|
|
174
|
-
current.ctimeNs === expected.ctimeNs
|
|
175
|
-
)
|
|
176
|
-
} catch {
|
|
177
|
-
return false
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/** Live protocol entries, pruning only unique paths whose process is gone. */
|
|
182
|
-
function liveEntries(
|
|
183
|
-
directory: string,
|
|
184
|
-
directoryIdentity: FileIdentity,
|
|
185
|
-
observe: FileLockOptions['observe'],
|
|
186
|
-
): LockEntry[] {
|
|
187
|
-
assertSameDirectory(directory, directoryIdentity)
|
|
188
|
-
const live: LockEntry[] = []
|
|
189
|
-
for (const name of readdirSync(directory)) {
|
|
190
|
-
const entry = parseEntry(name)
|
|
191
|
-
if (entry === null) continue
|
|
192
|
-
const entryPath = path.join(directory, name)
|
|
193
|
-
let stat
|
|
194
|
-
try {
|
|
195
|
-
stat = lstatSync(entryPath)
|
|
196
|
-
} catch (err) {
|
|
197
|
-
if ((err as NodeJS.ErrnoException).code === 'ENOENT') continue
|
|
198
|
-
throw err
|
|
199
|
-
}
|
|
200
|
-
const uid = currentUid()
|
|
201
|
-
if (stat.isSymbolicLink() || !stat.isFile() || (uid !== undefined && stat.uid !== uid)) {
|
|
202
|
-
throw new Error(`${entryPath} is not a current-user-owned regular file-lock entry.`)
|
|
203
|
-
}
|
|
204
|
-
if (processIsAlive(entry.pid)) {
|
|
205
|
-
live.push(entry)
|
|
206
|
-
continue
|
|
207
|
-
}
|
|
208
|
-
observe?.({ phase: 'stale-entry', entry: name })
|
|
209
|
-
try {
|
|
210
|
-
// Recheck the unique dead owner's inode so delayed recovery cannot reap
|
|
211
|
-
// a replacement published at the same path by external interference.
|
|
212
|
-
const current = lstatSync(entryPath)
|
|
213
|
-
if (current.dev === stat.dev && current.ino === stat.ino) unlinkSync(entryPath)
|
|
214
|
-
} catch {
|
|
215
|
-
// Another contender already recovered this exact dead owner's unique path.
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
return live
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function publishEmpty(file: string): EntryIdentity {
|
|
222
|
-
const handle = openSync(file, 'wx', 0o600)
|
|
223
|
-
try {
|
|
224
|
-
return entryIdentity(fstatSync(handle, { bigint: true }))
|
|
225
|
-
} finally {
|
|
226
|
-
closeSync(handle)
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* True when a publication attempt lost its rendezvous rather than found a
|
|
232
|
-
* broken one.
|
|
233
|
-
*
|
|
234
|
-
* A releaser removes an empty rendezvous directory the moment the last entry
|
|
235
|
-
* goes, so a contender registering at that instant sees the removal from
|
|
236
|
-
* whichever side it reached first. All three are the same event: `ENOENT` when
|
|
237
|
-
* the directory is already gone, `EINVAL` when APFS reports a directory that is
|
|
238
|
-
* still being deleted, and a replaced identity when a third contender recreated
|
|
239
|
-
* it first. None of them says anything about the lock's integrity, because a
|
|
240
|
-
* contender with no published entry owns nothing yet.
|
|
241
|
-
*/
|
|
242
|
-
function lostTheRendezvous(err: unknown): boolean {
|
|
243
|
-
if (err instanceof RendezvousReplaced) return true
|
|
244
|
-
const code = (err as NodeJS.ErrnoException).code
|
|
245
|
-
return code === 'ENOENT' || code === 'EINVAL'
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function publishChoosing(
|
|
249
|
-
directory: string,
|
|
250
|
-
name: string,
|
|
251
|
-
deadline: number,
|
|
252
|
-
observe: FileLockOptions['observe'],
|
|
253
|
-
): { file: string; fileIdentity: EntryIdentity; directoryIdentity: FileIdentity } {
|
|
254
|
-
const file = path.join(directory, name)
|
|
255
|
-
for (;;) {
|
|
256
|
-
try {
|
|
257
|
-
mkdirSync(directory, { recursive: true, mode: 0o700 })
|
|
258
|
-
const registrar = assertOwnedDirectory(directory)
|
|
259
|
-
chmodSync(directory, 0o700)
|
|
260
|
-
// Recover before registration so a delayed stale cleanup can only name an
|
|
261
|
-
// old owner's unique path, never this contender's future one.
|
|
262
|
-
liveEntries(directory, registrar, observe)
|
|
263
|
-
observe?.({ phase: 'registering', entry: name })
|
|
264
|
-
const fileIdentity = publishEmpty(file)
|
|
265
|
-
// Pin the rendezvous that holds the entry, not the one scanned before it
|
|
266
|
-
// existed. `open` resolves the path atomically, so the entry lands in
|
|
267
|
-
// whichever directory the path named at that instant — and from then on
|
|
268
|
-
// that directory is non-empty, so no cooperating releaser can remove it.
|
|
269
|
-
// Reading the identity here therefore names the contender's real
|
|
270
|
-
// rendezvous even when the scanned one was removed and recreated
|
|
271
|
-
// underneath it.
|
|
272
|
-
const directoryIdentity = assertOwnedDirectory(directory)
|
|
273
|
-
if (!sameEntryIdentity(file, fileIdentity)) {
|
|
274
|
-
throw new RendezvousReplaced(`${directory} no longer holds ${name}.`)
|
|
275
|
-
}
|
|
276
|
-
return { file, fileIdentity, directoryIdentity }
|
|
277
|
-
} catch (err) {
|
|
278
|
-
if (!lostTheRendezvous(err)) throw err
|
|
279
|
-
// Recreate it and register again; the deadline, not the number of losses,
|
|
280
|
-
// decides when contention becomes a failure.
|
|
281
|
-
if (Date.now() >= deadline) {
|
|
282
|
-
throw new Error(
|
|
283
|
-
`timed out publishing file lock contender in ${directory}: ${(err as Error).message}`,
|
|
284
|
-
)
|
|
285
|
-
}
|
|
286
|
-
Atomics.wait(lockSleep, 0, 0, FILE_LOCK_POLL_MS)
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function comesBefore(entry: TicketEntry, ticket: bigint, pid: number, token: string): boolean {
|
|
292
|
-
if (entry.ticket !== ticket) return entry.ticket < ticket
|
|
293
|
-
if (entry.pid !== pid) return entry.pid < pid
|
|
294
|
-
return entry.token < token
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
export function withFileLock<T>(file: string, action: () => T, options: FileLockOptions = {}): T {
|
|
298
|
-
const waitMs = options.waitMs ?? FILE_LOCK_WAIT_MS
|
|
299
|
-
const deadline = Date.now() + waitMs
|
|
300
|
-
const pid = process.pid
|
|
301
|
-
const token = randomBytes(FILE_LOCK_TOKEN_BYTES).toString('hex')
|
|
302
|
-
const choosingName = `choosing-${pid}-${token}`
|
|
303
|
-
let ownedPath: string | null = null
|
|
304
|
-
let ownedIdentity: EntryIdentity | null = null
|
|
305
|
-
let directoryIdentity: FileIdentity | null = null
|
|
306
|
-
let operationFailed = false
|
|
307
|
-
|
|
308
|
-
try {
|
|
309
|
-
const published = publishChoosing(file, choosingName, deadline, options.observe)
|
|
310
|
-
ownedPath = published.file
|
|
311
|
-
ownedIdentity = published.fileIdentity
|
|
312
|
-
directoryIdentity = published.directoryIdentity
|
|
313
|
-
options.observe?.({ phase: 'choosing-published', entry: choosingName })
|
|
314
|
-
let highest = 0n
|
|
315
|
-
for (const entry of liveEntries(file, directoryIdentity, options.observe)) {
|
|
316
|
-
if (entry.kind === 'ticket' && entry.ticket > highest) highest = entry.ticket
|
|
317
|
-
}
|
|
318
|
-
const ticket = highest + 1n
|
|
319
|
-
const ticketName = `ticket-${ticket}-${pid}-${token}`
|
|
320
|
-
const ticketPath = path.join(file, ticketName)
|
|
321
|
-
|
|
322
|
-
// The rename is the doorway: observers see either a chooser or its complete
|
|
323
|
-
// ticket, never a gap in which a live contender has no published state.
|
|
324
|
-
renameSync(ownedPath, ticketPath)
|
|
325
|
-
ownedPath = ticketPath
|
|
326
|
-
ownedIdentity = entryIdentity(lstatSync(ticketPath, { bigint: true }))
|
|
327
|
-
|
|
328
|
-
for (;;) {
|
|
329
|
-
const blockers: string[] = []
|
|
330
|
-
for (const entry of liveEntries(file, directoryIdentity, options.observe)) {
|
|
331
|
-
if (entry.pid === pid && entry.token === token) continue
|
|
332
|
-
if (entry.kind === 'choosing' || comesBefore(entry, ticket, pid, token)) {
|
|
333
|
-
blockers.push(entry.name)
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
if (blockers.length === 0) {
|
|
337
|
-
assertSameDirectory(file, directoryIdentity)
|
|
338
|
-
return action()
|
|
339
|
-
}
|
|
340
|
-
options.observe?.({ phase: 'waiting', blockers })
|
|
341
|
-
if (Date.now() >= deadline) throw new Error(`timed out waiting for file lock ${file}`)
|
|
342
|
-
Atomics.wait(lockSleep, 0, 0, FILE_LOCK_POLL_MS)
|
|
343
|
-
}
|
|
344
|
-
} catch (err) {
|
|
345
|
-
operationFailed = true
|
|
346
|
-
throw err
|
|
347
|
-
} finally {
|
|
348
|
-
let releaseChanged = false
|
|
349
|
-
if (ownedPath !== null && ownedIdentity !== null) {
|
|
350
|
-
try {
|
|
351
|
-
if (sameEntryIdentity(ownedPath, ownedIdentity)) unlinkSync(ownedPath)
|
|
352
|
-
else releaseChanged = true
|
|
353
|
-
} catch {
|
|
354
|
-
// External cleanup cannot turn a release failure into a caller failure.
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
try {
|
|
358
|
-
if (directoryIdentity !== null) assertSameDirectory(file, directoryIdentity)
|
|
359
|
-
rmdirSync(file)
|
|
360
|
-
} catch {
|
|
361
|
-
// Another contender has published, or external cleanup already removed it.
|
|
362
|
-
}
|
|
363
|
-
if (releaseChanged && options.strictRelease === true && !operationFailed) {
|
|
364
|
-
throw new Error(`${ownedPath ?? file} changed while held; refusing to remove the replacement.`)
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* Serialize a read/merge/write transaction for one user-owned target file.
|
|
371
|
-
*
|
|
372
|
-
* This is the single installer-facing lock policy: the target's direct parent
|
|
373
|
-
* is a real current-user-owned directory, remains the same inode throughout,
|
|
374
|
-
* and the bakery rendezvous lives beside the target rather than inside it.
|
|
375
|
-
*/
|
|
376
|
-
export function withTargetFileLock<T>(
|
|
377
|
-
target: string,
|
|
378
|
-
action: () => T,
|
|
379
|
-
options: Omit<FileLockOptions, 'strictRelease'> = {},
|
|
380
|
-
): T {
|
|
381
|
-
const parent = path.dirname(target)
|
|
382
|
-
mkdirSync(parent, { recursive: true })
|
|
383
|
-
const parentIdentity = assertOwnedDirectory(parent)
|
|
384
|
-
const lockDirectory = path.join(parent, `.${path.basename(target)}.notifai.lock`)
|
|
385
|
-
return withFileLock(
|
|
386
|
-
lockDirectory,
|
|
387
|
-
() => {
|
|
388
|
-
assertSameDirectory(parent, parentIdentity)
|
|
389
|
-
return action()
|
|
390
|
-
},
|
|
391
|
-
{ ...options, strictRelease: true },
|
|
392
|
-
)
|
|
393
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"vendor": "NousResearch/hermes-agent",
|
|
3
|
-
"release": "0.20.6",
|
|
4
|
-
"commit": "5f75ec197b16d1a055688fb9df63ac49586a2230",
|
|
5
|
-
"instance": {
|
|
6
|
-
"harness": "hermes",
|
|
7
|
-
"surface": "classic-cli",
|
|
8
|
-
"terminalBackend": "local"
|
|
9
|
-
},
|
|
10
|
-
"durableSessionId": "20260828_111302_29a404",
|
|
11
|
-
"observedToolSubprocessEnv": {
|
|
12
|
-
"HERMES_SESSION_ID": "20260828_111302_29a404"
|
|
13
|
-
},
|
|
14
|
-
"cwd": {
|
|
15
|
-
"invocation": "/workspace",
|
|
16
|
-
"shellHookCwdIsProcessCwd": true,
|
|
17
|
-
"promptSectionCwdRequiresTerminalOverride": true
|
|
18
|
-
},
|
|
19
|
-
"evidence": {
|
|
20
|
-
"nativeHostOs": "macos",
|
|
21
|
-
"portableFixtureFamilies": ["posix", "windows"],
|
|
22
|
-
"nativeOsUnrun": ["linux", "windows"]
|
|
23
|
-
},
|
|
24
|
-
"compression": {
|
|
25
|
-
"defaultMode": "in-place",
|
|
26
|
-
"sessionIdBefore": "20260828_120000_inplace",
|
|
27
|
-
"sessionIdAfter": "20260828_120000_inplace",
|
|
28
|
-
"legacyRotatingSessionId": "20260828_111114_88ae83"
|
|
29
|
-
},
|
|
30
|
-
"supported": ["deliberate-send", "source-context-from-invocation-cwd"],
|
|
31
|
-
"unsupported": [
|
|
32
|
-
"managed-setup",
|
|
33
|
-
"lifecycle-activation",
|
|
34
|
-
"question-routing"
|
|
35
|
-
],
|
|
36
|
-
"deferred": [
|
|
37
|
-
"tui",
|
|
38
|
-
"gateway",
|
|
39
|
-
"api",
|
|
40
|
-
"acp-session",
|
|
41
|
-
"safe-mode",
|
|
42
|
-
"native-linux",
|
|
43
|
-
"native-windows",
|
|
44
|
-
"live-auto-compression",
|
|
45
|
-
"interactive-reset",
|
|
46
|
-
"docker-backend",
|
|
47
|
-
"ssh-backend",
|
|
48
|
-
"other-remote-backends"
|
|
49
|
-
]
|
|
50
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"vendor": "openclaw/openclaw",
|
|
3
|
-
"release": "2026.7.1-2",
|
|
4
|
-
"commit": "0790d9f",
|
|
5
|
-
"instance": {
|
|
6
|
-
"harness": "openclaw",
|
|
7
|
-
"surface": "agent-local",
|
|
8
|
-
"execHost": "gateway"
|
|
9
|
-
},
|
|
10
|
-
"durableSessionKey": "agent:main:probe-owner",
|
|
11
|
-
"rotatingSessionId": "ae6686a4-d632-4750-b73b-56faec5fff9a",
|
|
12
|
-
"observedPluginEnv": {
|
|
13
|
-
"NOTIFAI_ACTIVE_HARNESS": "openclaw",
|
|
14
|
-
"NOTIFAI_ACTIVE_SESSION_ID": "agent:main:probe-owner"
|
|
15
|
-
},
|
|
16
|
-
"worker": {
|
|
17
|
-
"sessionKeySubstring": ":subagent:",
|
|
18
|
-
"provedSessionKey": "agent:main:subagent:probe-worker",
|
|
19
|
-
"missingIdentity": "fail-closed-worker"
|
|
20
|
-
},
|
|
21
|
-
"hooks": {
|
|
22
|
-
"proven": {
|
|
23
|
-
"activation": "before_prompt_build",
|
|
24
|
-
"stop": "agent_end",
|
|
25
|
-
"execMarkers": "resolve_exec_env"
|
|
26
|
-
},
|
|
27
|
-
"unprovenOnThisSurface": {
|
|
28
|
-
"presence": "message_received",
|
|
29
|
-
"sessionEnd": "session_end"
|
|
30
|
-
}
|
|
31
|
-
},
|
|
32
|
-
"evidence": {
|
|
33
|
-
"nativeHostOs": "macos",
|
|
34
|
-
"command": "openclaw agent --local",
|
|
35
|
-
"nativeOsUnrun": ["linux", "windows"]
|
|
36
|
-
},
|
|
37
|
-
"supported": [
|
|
38
|
-
"managed-setup",
|
|
39
|
-
"lifecycle-activation",
|
|
40
|
-
"source-context-from-session-key"
|
|
41
|
-
],
|
|
42
|
-
"unsupported": [
|
|
43
|
-
"question-routing",
|
|
44
|
-
"command-stop-as-turn-end",
|
|
45
|
-
"2026.2.26-cli"
|
|
46
|
-
],
|
|
47
|
-
"deferred": [
|
|
48
|
-
"gateway-embedded",
|
|
49
|
-
"gateway-remote",
|
|
50
|
-
"agent-exec",
|
|
51
|
-
"acp-nested",
|
|
52
|
-
"exec-sandbox",
|
|
53
|
-
"exec-node",
|
|
54
|
-
"native-linux",
|
|
55
|
-
"native-windows"
|
|
56
|
-
]
|
|
57
|
-
}
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
const FNV1A_64_OFFSET = 0xcbf29ce484222325n
|
|
2
|
-
const FNV1A_64_PRIME = 0x100000001b3n
|
|
3
|
-
const WORD_COUNT = 64n
|
|
4
|
-
|
|
5
|
-
/** Frozen word lists keep generated fallbacks stable for a given opaque id. */
|
|
6
|
-
const ADJECTIVES = [
|
|
7
|
-
'Amber',
|
|
8
|
-
'Arctic',
|
|
9
|
-
'Ash',
|
|
10
|
-
'Azure',
|
|
11
|
-
'Blue',
|
|
12
|
-
'Bold',
|
|
13
|
-
'Bright',
|
|
14
|
-
'Bronze',
|
|
15
|
-
'Calm',
|
|
16
|
-
'Cedar',
|
|
17
|
-
'Clear',
|
|
18
|
-
'Copper',
|
|
19
|
-
'Coral',
|
|
20
|
-
'Crisp',
|
|
21
|
-
'Dawn',
|
|
22
|
-
'Deep',
|
|
23
|
-
'Ember',
|
|
24
|
-
'Fern',
|
|
25
|
-
'Frost',
|
|
26
|
-
'Gentle',
|
|
27
|
-
'Golden',
|
|
28
|
-
'Green',
|
|
29
|
-
'Harbor',
|
|
30
|
-
'Hazel',
|
|
31
|
-
'Indigo',
|
|
32
|
-
'Ivory',
|
|
33
|
-
'Jade',
|
|
34
|
-
'Keen',
|
|
35
|
-
'Lively',
|
|
36
|
-
'Lunar',
|
|
37
|
-
'Maple',
|
|
38
|
-
'Meadow',
|
|
39
|
-
'Mellow',
|
|
40
|
-
'Misty',
|
|
41
|
-
'Noble',
|
|
42
|
-
'Ocean',
|
|
43
|
-
'Olive',
|
|
44
|
-
'Onyx',
|
|
45
|
-
'Quiet',
|
|
46
|
-
'Rapid',
|
|
47
|
-
'Red',
|
|
48
|
-
'River',
|
|
49
|
-
'Royal',
|
|
50
|
-
'Sage',
|
|
51
|
-
'Silver',
|
|
52
|
-
'Solar',
|
|
53
|
-
'Steady',
|
|
54
|
-
'Stone',
|
|
55
|
-
'Sunny',
|
|
56
|
-
'Swift',
|
|
57
|
-
'Teal',
|
|
58
|
-
'Timber',
|
|
59
|
-
'True',
|
|
60
|
-
'Velvet',
|
|
61
|
-
'Verdant',
|
|
62
|
-
'Violet',
|
|
63
|
-
'Warm',
|
|
64
|
-
'Wild',
|
|
65
|
-
'Winter',
|
|
66
|
-
'Wise',
|
|
67
|
-
'Witty',
|
|
68
|
-
'Yellow',
|
|
69
|
-
'Young',
|
|
70
|
-
'Zealous',
|
|
71
|
-
] as const
|
|
72
|
-
|
|
73
|
-
const NOUNS = [
|
|
74
|
-
'Alpaca',
|
|
75
|
-
'Antelope',
|
|
76
|
-
'Badger',
|
|
77
|
-
'Bear',
|
|
78
|
-
'Beaver',
|
|
79
|
-
'Bison',
|
|
80
|
-
'Bobcat',
|
|
81
|
-
'Caribou',
|
|
82
|
-
'Cougar',
|
|
83
|
-
'Coyote',
|
|
84
|
-
'Crane',
|
|
85
|
-
'Deer',
|
|
86
|
-
'Dolphin',
|
|
87
|
-
'Dragonfly',
|
|
88
|
-
'Eagle',
|
|
89
|
-
'Elk',
|
|
90
|
-
'Falcon',
|
|
91
|
-
'Finch',
|
|
92
|
-
'Firefly',
|
|
93
|
-
'Fox',
|
|
94
|
-
'Gazelle',
|
|
95
|
-
'Gecko',
|
|
96
|
-
'Hare',
|
|
97
|
-
'Heron',
|
|
98
|
-
'Ibex',
|
|
99
|
-
'Jay',
|
|
100
|
-
'Kestrel',
|
|
101
|
-
'Kingfisher',
|
|
102
|
-
'Koala',
|
|
103
|
-
'Lark',
|
|
104
|
-
'Leopard',
|
|
105
|
-
'Lynx',
|
|
106
|
-
'Magpie',
|
|
107
|
-
'Mantis',
|
|
108
|
-
'Marten',
|
|
109
|
-
'Moose',
|
|
110
|
-
'Osprey',
|
|
111
|
-
'Otter',
|
|
112
|
-
'Owl',
|
|
113
|
-
'Panda',
|
|
114
|
-
'Panther',
|
|
115
|
-
'Pelican',
|
|
116
|
-
'Penguin',
|
|
117
|
-
'Puma',
|
|
118
|
-
'Quail',
|
|
119
|
-
'Rabbit',
|
|
120
|
-
'Raven',
|
|
121
|
-
'Robin',
|
|
122
|
-
'Sable',
|
|
123
|
-
'Salmon',
|
|
124
|
-
'Seal',
|
|
125
|
-
'Shark',
|
|
126
|
-
'Sparrow',
|
|
127
|
-
'Stag',
|
|
128
|
-
'Swan',
|
|
129
|
-
'Tiger',
|
|
130
|
-
'Tortoise',
|
|
131
|
-
'Trout',
|
|
132
|
-
'Turtle',
|
|
133
|
-
'Viper',
|
|
134
|
-
'Whale',
|
|
135
|
-
'Wolf',
|
|
136
|
-
'Wren',
|
|
137
|
-
'Yak',
|
|
138
|
-
] as const
|
|
139
|
-
|
|
140
|
-
function fnv1a64(value: string): bigint {
|
|
141
|
-
let hash = FNV1A_64_OFFSET
|
|
142
|
-
for (const byte of new TextEncoder().encode(value)) {
|
|
143
|
-
hash ^= BigInt(byte)
|
|
144
|
-
hash = BigInt.asUintN(64, hash * FNV1A_64_PRIME)
|
|
145
|
-
}
|
|
146
|
-
return hash
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/** Deterministic fallback for a session whose work has no semantic title. */
|
|
150
|
-
export function generatedSessionLabel(sessionId: string): string {
|
|
151
|
-
const hash = fnv1a64(sessionId)
|
|
152
|
-
const adjective = ADJECTIVES[Number(hash % WORD_COUNT)]
|
|
153
|
-
const noun = NOUNS[Number((hash >> 6n) % WORD_COUNT)]
|
|
154
|
-
return `${adjective} ${noun}`
|
|
155
|
-
}
|