@primafuture/systemd-tasks 1.0.0 → 1.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,45 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.0 — 2026-10-10
4
+
5
+ - Add observed `finalizeAfterCompletion(reference, { afterRelease, timeoutMs })`:
6
+ an opt-in transient systemd-owned finalizer survives caller detach and death,
7
+ confirms payload/guardian cleanup, releases existing retention, and executes
8
+ one bounded native after-release command with finite stdin.
9
+ - Bind repeated registrations to the exact reference and callback, preserve
10
+ truthful registration outcomes, and collect the finalizer itself after exit.
11
+ - Cover post-payload reads with the same finalization deadline and honor native
12
+ callback stop grace through manager-owned cgroup termination.
13
+ - Keep default retention, existing intent/reference formats, manual lifecycle,
14
+ and output ownership unchanged.
15
+
16
+ ## 1.1.0 — 2026-09-17 (local candidate)
17
+
18
+ - Add the `@primafuture/systemd-tasks/observed` entrypoint with immutable
19
+ JSON-safe intents, finite manager-owned stdin, live binary stdout/stderr, and
20
+ separate payload/guardian lifecycle evidence.
21
+ - Create payload, retention, and `Type=notify` guardian services in one atomic
22
+ transaction. Both process services require successful retention initialization;
23
+ payload stop propagates to a drain-aware guardian even when exec was cancelled.
24
+ Fail closed when the guardian dies, while allowing it to drain output after
25
+ payload exit, stop, timeout, client detach, or client death.
26
+ - Add a versioned bounded guardian protocol, sequential asynchronous callback
27
+ backpressure, explicit local detach, and stable `OBSERVATION_FAILED` errors.
28
+ Reconnect, replay, and stream adoption remain deliberately unsupported.
29
+ - Generalize the existing D-Bus adapter to an ordered auxiliary-unit topology and
30
+ share transport ownership, mutation serialization, notification routing,
31
+ cleanup, diagnostics, and `TaskError` identity across root/observed APIs and
32
+ mixed ESM/CommonJS loading.
33
+ - Upgrade the external native transport to the exact published
34
+ `@primafuture/socket-fdx@1.1.0` release and use its owned `socketPair()` and
35
+ `adoptSocket()` primitives.
36
+ - Extend deterministic, real-systemd, installed ESM/CommonJS, and TypeScript
37
+ consumer tests, including the real 512 KiB stdin boundary, partial socket
38
+ allocation rollback, pre-readiness detach, cross-client pre-exec stop, final
39
+ output under slow-callback backpressure during explicit stop, 32 MiB per-channel draining, and guardian
40
+ fail-closed behavior. Detach also settles independently of an already-pending
41
+ application callback. No npm publish was performed.
42
+
3
43
  ## 1.0.0 — 2026-09-07
4
44
 
5
45
  - Add explicit preparation, single-attempt submission, lookup, inspection, waiting,
package/README.md CHANGED
@@ -4,7 +4,7 @@ Run one-shot Linux processes owned by the **systemd user manager**, with literal
4
4
  arguments, binary file-descriptor I/O, manager-owned timeouts, and recoverable
5
5
  results after the Node client exits or crashes.
6
6
 
7
- Version 1.0.0 requires Node 24.13.0+, Linux x64 with glibc 2.28+, systemd 257+,
7
+ Version 1.2.0 requires Node 24.13.0+, Linux x64 with glibc 2.28+, systemd 257+,
8
8
  and cgroup v2. The verified environment
9
9
  is Node 24.13.0, systemd 257.9-0ubuntu2.4, Linux 6.17.0-14-generic, x86-64.
10
10
  Other versions satisfying those minimums are not independently verified.
@@ -15,11 +15,12 @@ Other versions satisfying those minimums are not independently verified.
15
15
  npm install @primafuture/systemd-tasks
16
16
  ```
17
17
 
18
- The pinned native dependency `@primafuture/socket-fdx@1.0.0` is installed under
19
- the npm alias `usocket` and supplies its compatible `USocket` adapter. It ships a
20
- prebuilt native binary: ordinary installation needs no compiler, Python, or install
21
- scripts. Consumers do not need `overrides`. The dependency is pinned to its stable release;
22
- musl Linux and other architectures are outside its current support. The JavaScript D-Bus stack
18
+ The native dependency `@primafuture/socket-fdx@1.1.0` is installed under the npm
19
+ alias `usocket` and supplies both the D-Bus adapter and explicitly owned Unix
20
+ socket-pair primitives. It is pinned to the exact published release and ships a
21
+ prebuilt native binary, so ordinary installation needs no compiler, Python, install
22
+ scripts, or consumer override. Musl Linux and other architectures remain unsupported.
23
+ The JavaScript D-Bus stack
23
24
  (`dbus-next@0.10.2` and its dependencies) is bundled; licenses are recorded in
24
25
  [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
25
26
 
@@ -29,6 +30,9 @@ that the retention executable resolves to a regular executable file before openi
29
30
  the bus, then verifies the manager environment. It fails explicitly; the library does not provision a manager,
30
31
  enable linger, request elevated privileges, or fall back to child_process.
31
32
  Importing either the ESM or CommonJS entrypoint does not connect or start a process.
33
+ Observed tasks additionally require `/usr/bin/systemd-notify`, `/usr/bin/systemctl`,
34
+ and the packaged guardian bundle; `connectObserved()` verifies them before opening
35
+ the bus.
32
36
 
33
37
  ## Basic execution
34
38
 
@@ -70,6 +74,133 @@ environment that are absent from this map, using a snapshot immediately before
70
74
  submission. Systemd may add execution metadata such as `INVOCATION_ID`. Concurrent
71
75
  external changes to manager configuration are outside this guarantee.
72
76
 
77
+ ## Observed execution
78
+
79
+ The separate `@primafuture/systemd-tasks/observed` entrypoint adds live binary
80
+ stdout/stderr delivery without wrapping the payload process:
81
+
82
+ ```ts
83
+ import * as observed from '@primafuture/systemd-tasks/observed';
84
+
85
+ const intent = observed.prepareObserved({
86
+ executable: '/usr/bin/sort',
87
+ argv: [],
88
+ cwd: '/tmp',
89
+ env: {},
90
+ runtimeLimitMs: 5000,
91
+ stopGraceMs: 500,
92
+ stdin: 'second\nfirst\n',
93
+ });
94
+
95
+ const client = await observed.connectObserved();
96
+ try {
97
+ const started = await client.startObserved(intent, {
98
+ async onOutput(event) {
99
+ // Awaiting here applies bounded backpressure to both output channels.
100
+ await consume(event.stream, event.data);
101
+ },
102
+ });
103
+ await started.observation.completion;
104
+ const completed = await client.wait(intent, { timeoutMs: 10000 });
105
+ await client.release(completed.reference);
106
+ } finally {
107
+ await client.disconnect();
108
+ }
109
+ ```
110
+
111
+ `stdin` is mandatory. A string is snapshotted as UTF-8, a `Uint8Array` is copied,
112
+ an empty value remains an explicit empty data input, and `null` selects `/dev/null`.
113
+ The fixed limit is 512 KiB, chosen to remain below systemd's manager-state line
114
+ serialization boundary after base64 expansion. The canonical base64 snapshot is
115
+ part of the serialized intent and its fingerprint. Systemd owns `StandardInputData` after submission and
116
+ may retain those bytes in manager state until release, so treat the intent and
117
+ manager access as sensitive when stdin contains secrets.
118
+
119
+ Each observed task consists of payload, retention, and `Type=notify` guardian
120
+ services created by one `StartTransientUnit` transaction. The payload is ordered
121
+ after and `BindsTo` the guardian; both process services require successful
122
+ retention initialization. `startObserved()` returns only after manager
123
+ acceptance and guardian readiness, with a fixed five-second readiness budget. A
124
+ failure after manager acceptance reports `mutationOutcome: 'accepted'`; it is not
125
+ permission to retry.
126
+
127
+ Output callbacks are sequential and their returned promises provide real bounded
128
+ backpressure. Callback failure rejects `observation.completion` with
129
+ `OBSERVATION_FAILED` and detaches locally; it does not stop the payload. `detach()`
130
+ and `disconnect()` likewise close only the local observer. The guardian continues
131
+ to drain and discard both channels so the payload does not receive `EPIPE` merely
132
+ because the client left. There is deliberately no reconnect, replay, or adoption
133
+ of another client's live stream. Use ordinary durable file I/O instead when output
134
+ must survive observation loss.
135
+
136
+ An already-invoked callback is application code and cannot be cancelled. Explicit
137
+ detach closes the socket and settles `completion` as `detached` without waiting for
138
+ that callback promise; no later callback is invoked, and a later settlement of the
139
+ detached callback cannot change the observation outcome or block `disconnect()`.
140
+
141
+ The guardian emits a private versioned binary protocol with separate stdout and
142
+ stderr frames. Payload bytes and callback causes are never included in diagnostics.
143
+ `wait()` reports payload and guardian results independently and reaches `completed`
144
+ only after both services are terminal with confirmed empty cgroups. `stop()` targets
145
+ the payload; systemd propagates that stop to the guardian in reverse dependency
146
+ order, so it drains bytes already written before completing. This also closes a
147
+ guardian whose payload start was cancelled before exec. `release()` refuses until
148
+ both process services are clean, then removes retention.
149
+
150
+ ### Automatic finalization of a background task
151
+
152
+ Observed clients can explicitly hand completion to the user manager:
153
+
154
+ ```ts
155
+ const found = await client.lookup(intent);
156
+ if (found.status === 'found' && found.snapshot.identity === 'complete') {
157
+ await client.finalizeAfterCompletion(found.reference, {
158
+ timeoutMs: 30000,
159
+ afterRelease: {
160
+ executable: '/absolute/path/to/node',
161
+ argv: ['/absolute/path/to/application.mjs', '--finish-owned-record'],
162
+ cwd: '/absolute/application/directory',
163
+ env: {},
164
+ runtimeLimitMs: 10000,
165
+ stopGraceMs: 1000,
166
+ stdin: JSON.stringify({ generation: 'application-owned-generation' }),
167
+ },
168
+ });
169
+ }
170
+ ```
171
+
172
+ Register after persisting the exact reference and before advertising the background
173
+ task as ready. A separate transient finalizer survives observer detach, CLI exit,
174
+ and death of the original client. It does not stop the original task. It waits for
175
+ payload termination and confirmed payload/guardian cleanup, uses the existing
176
+ `release()` checks, and then executes the finite native command once, without a
177
+ shell, inherited environment, or output replay. The command runs in the finalizer's
178
+ own systemd control group; its output is discarded. The finalizer is collected
179
+ after it exits, including failure. Safe error metadata remains in the journal.
180
+
181
+ `timeoutMs` is one shared deadline for lookup, cleanup, release, and the
182
+ after-release command once the finalizer confirms payload termination. It does
183
+ not bound the task's active lifetime or restart for each phase. Callback runtime
184
+ is also limited by its non-null `runtimeLimitMs`, when supplied. On timeout the
185
+ finalizer exits; systemd stops its remaining command processes with `SIGTERM`,
186
+ allows `stopGraceMs`, and escalates to `SIGKILL` only if necessary. Stop grace is
187
+ a separate bounded cleanup interval, not additional execution time. The callback
188
+ may not be a resident service. A timed-out read or release never starts the callback.
189
+
190
+ The receipt says `accepted` or `already-registered`, not `completed`. An identical
191
+ registration while the finalizer exists does not create another callback. A
192
+ different callback for the same task is a conflict. An unknown registration outcome
193
+ must be reconciled before proceeding; never blindly submit a replacement task.
194
+ Failed cleanup does not execute the callback. Callback failure does not restore
195
+ released task results or automatically retry the callback.
196
+
197
+ This is deliberately opt-in and does not change the default retention policy.
198
+ After handing finalization off, do not rely on a late `wait()` retaining an exit
199
+ result: release can win that race. Applications should confirm their own cleanup
200
+ effect plus absence of the exact task rather than fabricate an exit status. The
201
+ callback must condition its own changes on its persisted generation. Recovery is
202
+ still required for a client death before registration and for manager restart.
203
+
73
204
  ## Lifecycle and recovery
74
205
 
75
206
  | Operation | Contract |
@@ -83,6 +214,7 @@ external changes to manager configuration are outside this guarantee.
83
214
  | `stop(reference, options?)` | Request manager stop; `wait: true` additionally waits for cleanup. |
84
215
  | `release(reference, options?)` | Refuse live tasks, then discard owned result retention. Never deletes logs. |
85
216
  | `disconnect()` | Release local socket/listeners only; tasks keep running. Idempotent. |
217
+ | Observed `finalizeAfterCompletion(reference, options)` | Register exact manager-owned cleanup followed by one finite native command. Opt-in, not a completion receipt. |
86
218
 
87
219
  Save the **intent before calling start**, then save an observed reference and final
88
220
  result as they become available. Intents contain the command and environment,
@@ -206,7 +338,7 @@ service manipulation can invalidate evidence. The library does not enable linger
206
338
  recreate missing tasks, implement a daemon, or promise survival when the manager
207
339
  itself stops. Connection failure is distinct from missing units.
208
340
 
209
- The only advanced properties allowed in 1.0.0 are `CPUWeight` and `IOWeight`, each
341
+ The only advanced properties allowed in 1.x are `CPUWeight` and `IOWeight`, each
210
342
  with signature `'t'` and an integer value from 1 to 10000. Duplicates, unknown
211
343
  properties, and lifecycle overrides are rejected before mutation. Kernel/controller
212
344
  configuration determines the practical scheduling effect of those weights.
@@ -228,7 +360,8 @@ npm run check
228
360
  `check` runs strict typechecking, unit tests, real systemd integration tests, dual
229
361
  ESM/CJS build, and tarball runtime/type smoke tests. `prepublishOnly` invokes the
230
362
  same chain. Integration tests fail explicitly when prerequisites are missing.
231
- Tests create unique identities and clean only their recorded task/retention pairs.
363
+ Tests create unique identities and clean only their recorded task/retention pairs,
364
+ plus the exact guardian identity for observed tests.
232
365
  They do not restart the user manager or change host configuration.
233
366
 
234
367
  Runnable repository examples after `npm run build`:
@@ -25,7 +25,7 @@ This file is generated by `scripts/licenses.ts` from the distributed source map.
25
25
 
26
26
  ## External native dependency
27
27
 
28
- `@primafuture/socket-fdx` 1.0.0 (ISC) is installed separately under the npm alias `usocket`. It ships its native binary, license, and third-party notices, has no runtime dependencies, and is not embedded in this JavaScript bundle. The direct pinned alias applies in consumer installations without root overrides.
28
+ `@primafuture/socket-fdx` 1.1.0 (ISC) is installed separately under the npm alias `usocket`. It ships its native binary, license, and third-party notices, has no runtime dependencies, and is not embedded in this JavaScript bundle. The direct pinned alias applies in consumer installations without root overrides.
29
29
 
30
30
  ## @nornagon/put 0.0.8
31
31