@awebai/oats 0.32.0 → 0.33.0
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 +1 -1
- package/bin/oats.mjs +30 -15
- package/docs/capabilities.md +5 -0
- package/docs/configuration.md +2 -2
- package/docs/design/2026-09-23-workspace-module-contracts.md +3 -2
- package/docs/desktop-cli-api.md +50 -6
- package/docs/first-team.md +20 -2
- package/docs/implementation.md +64 -2
- package/docs/integrations.md +1 -1
- package/docs/official-catalog.md +2 -2
- package/docs/packages.md +2 -2
- package/docs/release-notes/v0.33.0.md +174 -0
- package/docs/souls-and-instances.md +64 -0
- package/docs/workspaces.md +1 -1
- package/lib/core.mjs +89 -17
- package/lib/harness-trust.mjs +139 -0
- package/lib/instance-inspect.mjs +16 -2
- package/lib/process-group.mjs +54 -0
- package/lib/remote.mjs +594 -102
- package/package-catalog.json +2 -2
- package/package.json +1 -1
package/lib/process-group.mjs
CHANGED
|
@@ -10,3 +10,57 @@ export function killGroup(child, signal = "SIGKILL") {
|
|
|
10
10
|
try { process.kill(pid, signal); } catch { /* already gone */ }
|
|
11
11
|
return true;
|
|
12
12
|
}
|
|
13
|
+
|
|
14
|
+
/** How long a group asked to end with SIGTERM gets before SIGKILL. */
|
|
15
|
+
export const TERM_GRACE_MS = 2_000;
|
|
16
|
+
|
|
17
|
+
/** Children whose 'close' has fired (watchGroup): the leader exited and was reaped, its stdio pipes shut. */
|
|
18
|
+
const closedGroups = new WeakSet();
|
|
19
|
+
|
|
20
|
+
/** Record a detached child's 'close': call it right after spawning a child that terminateGroup may end. → the child. */
|
|
21
|
+
export function watchGroup(child) {
|
|
22
|
+
child.once("close", () => closedGroups.add(child));
|
|
23
|
+
return child;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** How often terminateGroup checks, after the leader's 'close', whether its group is empty yet. */
|
|
27
|
+
export const GROUP_PROBE_MS = 50;
|
|
28
|
+
|
|
29
|
+
/** Whether a detached child's process group still has a member we may signal (ESRCH: empty; EPERM: not ours). */
|
|
30
|
+
function groupAlive(child) {
|
|
31
|
+
try { process.kill(-child.pid, 0); return true; } catch { return false; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** End a detached child's whole process group gracefully: SIGTERM now, SIGKILL to the group after `graceMs`.
|
|
35
|
+
* git removes its own lock files (`shallow.lock`, `config.lock`, a ref's `.lock`) on SIGTERM, never on SIGKILL:
|
|
36
|
+
* a git killed outright leaves a lock that blocks every later write. The timers are unref'd, so they never keep
|
|
37
|
+
* the process alive. A group seen empty is never signalled again: its id may then lead an unrelated group.
|
|
38
|
+
* Returns whether anything was signalled. */
|
|
39
|
+
export function terminateGroup(child, graceMs = TERM_GRACE_MS) {
|
|
40
|
+
// A child already closed may have left an empty group, whose id is free: send nothing.
|
|
41
|
+
if (closedGroups.has(child)) return false;
|
|
42
|
+
if (!signalGroup(child, "SIGTERM")) return false;
|
|
43
|
+
// Until 'close', the group holds a pipe-holding member (git, or its ssh or remote helper), so its id is ours and
|
|
44
|
+
// the SIGKILL may come. After 'close' the leader is gone but a member that ignores SIGTERM and holds no pipe may
|
|
45
|
+
// remain: no pid is allocated while it is a live group's id, so a group seen non-empty is still ours. It is probed
|
|
46
|
+
// until the grace ends: empty → no SIGKILL, ever; still there → SIGKILL. What remains is a group that empties and is
|
|
47
|
+
// reused within one GROUP_PROBE_MS.
|
|
48
|
+
let probe = null;
|
|
49
|
+
const done = () => { clearTimeout(timer); clearInterval(probe); };
|
|
50
|
+
const timer = setTimeout(() => { clearInterval(probe); signalGroup(child, "SIGKILL"); }, graceMs);
|
|
51
|
+
timer.unref?.();
|
|
52
|
+
child.once?.("close", () => {
|
|
53
|
+
if (!groupAlive(child)) { done(); return; }
|
|
54
|
+
probe = setInterval(() => { if (!groupAlive(child)) done(); }, GROUP_PROBE_MS);
|
|
55
|
+
probe.unref?.();
|
|
56
|
+
});
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Signal a detached child's process group only (see killGroup for the pid-0 guard). → whether it was a real child. */
|
|
61
|
+
export function signalGroup(child, signal) {
|
|
62
|
+
const pid = child?.pid;
|
|
63
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return false;
|
|
64
|
+
try { process.kill(-pid, signal); } catch { /* the group is gone */ }
|
|
65
|
+
return true;
|
|
66
|
+
}
|