things-api 0.19.3 → 0.20.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/deputy/prebuilt/Things API Helper.app/Contents/CodeResources +0 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/Helpers/things-reader.app/Contents/MacOS/things-reader +0 -0
- package/deputy/prebuilt/Things API Helper.app/Contents/MacOS/things-deputy +0 -0
- package/dist/audit/schema.d.ts +42 -10
- package/dist/audit/schema.js +21 -0
- package/dist/audit/schema.js.map +1 -1
- package/dist/cli/commands/doctor.js +3 -1
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/op-result.js +25 -7
- package/dist/cli/commands/op-result.js.map +1 -1
- package/dist/cli/commands/repeat-flags.d.ts +19 -1
- package/dist/cli/commands/repeat-flags.js +71 -0
- package/dist/cli/commands/repeat-flags.js.map +1 -1
- package/dist/cli/commands/rescue.d.ts +12 -0
- package/dist/cli/commands/rescue.js +83 -0
- package/dist/cli/commands/rescue.js.map +1 -0
- package/dist/cli/commands/writes.js +86 -32
- package/dist/cli/commands/writes.js.map +1 -1
- package/dist/cli/help.js +25 -25
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/main.js +12 -2
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/resolve-invocation.d.ts +9 -1
- package/dist/cli/resolve-invocation.js +71 -0
- package/dist/cli/resolve-invocation.js.map +1 -1
- package/dist/cli/unknown-command.d.ts +26 -0
- package/dist/cli/unknown-command.js +135 -0
- package/dist/cli/unknown-command.js.map +1 -0
- package/dist/contracts.d.ts +4 -3
- package/dist/contracts.js +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +62 -10
- package/dist/mcp/server.js.map +1 -1
- package/dist/model/entities.d.ts +11 -0
- package/dist/model/entities.js.map +1 -1
- package/dist/model/mappers.js +15 -0
- package/dist/model/mappers.js.map +1 -1
- package/dist/op-result.d.ts +24 -2
- package/dist/op-result.js +77 -20
- package/dist/op-result.js.map +1 -1
- package/dist/process-instance.d.ts +41 -0
- package/dist/process-instance.js +82 -0
- package/dist/process-instance.js.map +1 -0
- package/dist/rescue.d.ts +247 -0
- package/dist/rescue.js +811 -0
- package/dist/rescue.js.map +1 -0
- package/dist/session-grant.js +4 -15
- package/dist/session-grant.js.map +1 -1
- package/dist/ui-state.d.ts +8 -4
- package/dist/ui-state.js +8 -4
- package/dist/ui-state.js.map +1 -1
- package/dist/write/batch.js +4 -1
- package/dist/write/batch.js.map +1 -1
- package/dist/write/clone.js +12 -6
- package/dist/write/clone.js.map +1 -1
- package/dist/write/commands.d.ts +9 -0
- package/dist/write/commands.js +135 -50
- package/dist/write/commands.js.map +1 -1
- package/dist/write/disclosures.d.ts +275 -0
- package/dist/write/disclosures.js +277 -0
- package/dist/write/disclosures.js.map +1 -0
- package/dist/write/experimental.d.ts +16 -0
- package/dist/write/experimental.js +19 -0
- package/dist/write/experimental.js.map +1 -1
- package/dist/write/failure-hints.js +3 -2
- package/dist/write/failure-hints.js.map +1 -1
- package/dist/write/guards.d.ts +1 -1
- package/dist/write/guards.js +102 -3
- package/dist/write/guards.js.map +1 -1
- package/dist/write/heading.js +4 -7
- package/dist/write/heading.js.map +1 -1
- package/dist/write/lock.d.ts +85 -1
- package/dist/write/lock.js +121 -6
- package/dist/write/lock.js.map +1 -1
- package/dist/write/make-repeating-project.js +2 -1
- package/dist/write/make-repeating-project.js.map +1 -1
- package/dist/write/operations.d.ts +52 -6
- package/dist/write/operations.js +22 -0
- package/dist/write/operations.js.map +1 -1
- package/dist/write/opid.d.ts +69 -8
- package/dist/write/opid.js +116 -20
- package/dist/write/opid.js.map +1 -1
- package/dist/write/param-schema.js +14 -2
- package/dist/write/param-schema.js.map +1 -1
- package/dist/write/pipeline.d.ts +82 -3
- package/dist/write/pipeline.js +204 -60
- package/dist/write/pipeline.js.map +1 -1
- package/dist/write/planner.d.ts +7 -0
- package/dist/write/planner.js +12 -0
- package/dist/write/planner.js.map +1 -1
- package/dist/write/preserve-modified.d.ts +17 -8
- package/dist/write/preserve-modified.js.map +1 -1
- package/dist/write/promote-clone.js +106 -53
- package/dist/write/promote-clone.js.map +1 -1
- package/dist/write/reorder.js +45 -39
- package/dist/write/reorder.js.map +1 -1
- package/dist/write/repeat-rule.d.ts +25 -1
- package/dist/write/repeat-rule.js +69 -0
- package/dist/write/repeat-rule.js.map +1 -1
- package/dist/write/resolution-timestamps.js +104 -78
- package/dist/write/resolution-timestamps.js.map +1 -1
- package/dist/write/reversibility.js +2 -2
- package/dist/write/reversibility.js.map +1 -1
- package/dist/write/spawn-expectation.d.ts +135 -0
- package/dist/write/spawn-expectation.js +237 -0
- package/dist/write/spawn-expectation.js.map +1 -0
- package/dist/write/template-mutation.js +34 -35
- package/dist/write/template-mutation.js.map +1 -1
- package/dist/write/undo.js +45 -5
- package/dist/write/undo.js.map +1 -1
- package/dist/write/vectors/simulator.js +48 -2
- package/dist/write/vectors/simulator.js.map +1 -1
- package/dist/write/vectors/types.d.ts +12 -0
- package/dist/write/vectors/ui-recipes.js +11 -1
- package/dist/write/vectors/ui-recipes.js.map +1 -1
- package/dist/write/vectors/ui-state.d.ts +64 -6
- package/dist/write/vectors/ui-state.js +91 -4
- package/dist/write/vectors/ui-state.js.map +1 -1
- package/dist/write/vectors/ui.d.ts +69 -3
- package/dist/write/vectors/ui.js +293 -146
- package/dist/write/vectors/ui.js.map +1 -1
- package/dist/write/vectors/url-scheme.js +26 -0
- package/dist/write/vectors/url-scheme.js.map +1 -1
- package/dist/write/verify/delta.d.ts +17 -2
- package/dist/write/verify/delta.js +46 -16
- package/dist/write/verify/delta.js.map +1 -1
- package/dist/write/verify/poller.d.ts +3 -2
- package/dist/write/verify/poller.js +3 -1
- package/dist/write/verify/poller.js.map +1 -1
- package/package.json +1 -1
- package/schema/envelope.schema.json +1 -2
- package/skills/things-cli/SKILL.md +4 -2
- package/skills/things-cli/references/repeating.md +1 -1
- package/dist/cli/commands/ui-state.d.ts +0 -11
- package/dist/cli/commands/ui-state.js +0 -27
- package/dist/cli/commands/ui-state.js.map +0 -1
package/dist/op-result.js
CHANGED
|
@@ -14,17 +14,26 @@
|
|
|
14
14
|
* caller runs `things op-result <op-id>` in a FRESH short-lived process and reads
|
|
15
15
|
* the final outcome the killed process already durably wrote (fsync per record).
|
|
16
16
|
*
|
|
17
|
-
*
|
|
17
|
+
* Four outcomes:
|
|
18
18
|
* - FOUND — a final (non-intent) record carries the id: report its result code,
|
|
19
19
|
* target uuid, and post-verify observation (the standard fields the killed
|
|
20
20
|
* process would have printed).
|
|
21
|
-
* -
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* - IN-FLIGHT — an unsuperseded `intent` marker whose HOLDER IS ALIVE: the op is
|
|
22
|
+
* still running right now. The caller's move is to poll again, not to retry.
|
|
23
|
+
* - ORPHANED — an unsuperseded `intent` marker whose holder is GONE: the process
|
|
24
|
+
* died between touching the app and writing its final record, so the outcome is
|
|
25
|
+
* genuinely UNCERTAIN (the app-side change may have landed). The newest
|
|
24
26
|
* correlated trace file is surfaced when determinable.
|
|
25
27
|
* - UNKNOWN — no record carries the id: it never ran, the id is mistyped, or the
|
|
26
28
|
* record has aged out of the local history.
|
|
27
29
|
*
|
|
30
|
+
* Splitting the last two used to be impossible: an intent recorded no holder, so
|
|
31
|
+
* this could only hedge ("still running, OR died mid-flight") and the caller had
|
|
32
|
+
* to guess which. A keyed intent now carries the writing process's instance
|
|
33
|
+
* identity (#639), which makes it a question with an answer. The rare third
|
|
34
|
+
* shape — an intent with no holder recorded — keeps the old hedged status,
|
|
35
|
+
* because for that record the honest answer really is "cannot tell".
|
|
36
|
+
*
|
|
28
37
|
* This is a pure history read — it opens no database and drives nothing. Reused by
|
|
29
38
|
* the CLI subcommand and (read-only) the MCP surface through the single library
|
|
30
39
|
* entry point (src/index.ts), like {@link diagnose}.
|
|
@@ -32,6 +41,7 @@
|
|
|
32
41
|
import { readdirSync, statSync } from "node:fs";
|
|
33
42
|
import { join } from "node:path";
|
|
34
43
|
import { auditDir as defaultAuditDir, traceDir as defaultTraceDir } from "./paths.js";
|
|
44
|
+
import { instanceAlive } from "./process-instance.js";
|
|
35
45
|
import { readAuditRecords } from "./write/undo.js";
|
|
36
46
|
/** The trace file whose mtime is closest to `targetTs` (within a window), else null. */
|
|
37
47
|
function correlatedTrace(traceDir, targetTs) {
|
|
@@ -116,18 +126,26 @@ export function opResult(opId, options = {}) {
|
|
|
116
126
|
uuid: null,
|
|
117
127
|
observed: null,
|
|
118
128
|
verify: null,
|
|
129
|
+
steps: null,
|
|
119
130
|
ts: null,
|
|
120
131
|
durationMs: null,
|
|
121
132
|
tracePath: null,
|
|
133
|
+
holder: null,
|
|
122
134
|
note: `no change-history record carries op-id "${opId}" — it never ran, the id is mistyped, or ` +
|
|
123
135
|
"the record has aged out of the local history (`things doctor` shows the history health)",
|
|
124
136
|
};
|
|
125
137
|
}
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
138
|
+
// An intent is superseded by the final of its OWN attempt, paired by `ts` —
|
|
139
|
+
// both records of one attempt derive from the same `startedAt`, which is the
|
|
140
|
+
// schema's documented sibling invariant. Deliberately NOT "the last record
|
|
141
|
+
// wins": `readAuditRecords` RE-SORTS the trail by `ts`, so file order does not
|
|
142
|
+
// survive the read. `findPendingIntent` (write/opid.ts) applies the identical
|
|
143
|
+
// rule, so the reporting surface and the dispatch gate can never disagree
|
|
144
|
+
// about whether a key is in flight.
|
|
145
|
+
const settled = new Set(matched.filter((r) => r.result !== "intent").map((r) => r.ts));
|
|
146
|
+
const pending = matched.findLast((r) => r.result === "intent" && !settled.has(r.ts));
|
|
147
|
+
if (pending === undefined) {
|
|
148
|
+
const finals = matched.filter((r) => r.result !== "intent");
|
|
131
149
|
const rec = finals[finals.length - 1];
|
|
132
150
|
return {
|
|
133
151
|
opId,
|
|
@@ -138,29 +156,68 @@ export function opResult(opId, options = {}) {
|
|
|
138
156
|
...(rec.occurrence !== undefined && { occurrence: rec.occurrence }),
|
|
139
157
|
observed: rec.observed,
|
|
140
158
|
verify: rec.verify,
|
|
159
|
+
steps: rec.steps ?? null,
|
|
141
160
|
ts: rec.ts,
|
|
142
161
|
durationMs: rec.durationMs,
|
|
143
162
|
tracePath: null,
|
|
163
|
+
holder: null,
|
|
144
164
|
note: foundNote(rec.result, rec.uuid),
|
|
145
165
|
};
|
|
146
166
|
}
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
167
|
+
// An unsuperseded intent. Its holder decides which of the two very different
|
|
168
|
+
// situations this is — and the caller's next move differs completely between
|
|
169
|
+
// them, which is why the old single hedged answer was worth splitting.
|
|
170
|
+
const last = pending;
|
|
171
|
+
const tracePath = correlatedTrace(traceDir, last.ts);
|
|
172
|
+
const recorded = last.holder;
|
|
173
|
+
const holder = recorded === undefined ? null : { ...recorded, alive: instanceAlive(recorded) };
|
|
174
|
+
const target = last.uuid ?? "<uuid>";
|
|
175
|
+
const base = {
|
|
151
176
|
opId,
|
|
152
|
-
|
|
153
|
-
op: rec.op,
|
|
177
|
+
op: last.op,
|
|
154
178
|
result: null,
|
|
155
|
-
uuid:
|
|
179
|
+
uuid: last.uuid,
|
|
156
180
|
observed: null,
|
|
157
181
|
verify: null,
|
|
158
|
-
|
|
182
|
+
// An intent marker is written BEFORE the app is touched, so it never carries
|
|
183
|
+
// steps — the drive had not run yet. The trace file is the recovery here.
|
|
184
|
+
steps: null,
|
|
185
|
+
ts: last.ts,
|
|
159
186
|
durationMs: null,
|
|
160
187
|
tracePath,
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
188
|
+
holder,
|
|
189
|
+
};
|
|
190
|
+
if (holder !== null && holder.alive) {
|
|
191
|
+
return {
|
|
192
|
+
...base,
|
|
193
|
+
status: "in-flight",
|
|
194
|
+
note: `the operation is STILL RUNNING — it started ${last.ts} and the process that owns it ` +
|
|
195
|
+
`(pid ${holder.pid}) is alive. Nothing to recover yet: run this command again in a few ` +
|
|
196
|
+
"seconds until it reports a final outcome. Do NOT resubmit the write — a GUI-driven " +
|
|
197
|
+
"change takes seconds, and longer on a large or syncing database. (Resubmitting the same " +
|
|
198
|
+
"op-id while it runs is refused rather than executed, so a retry costs you the wait " +
|
|
199
|
+
"either way.)",
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
if (holder !== null) {
|
|
203
|
+
return {
|
|
204
|
+
...base,
|
|
205
|
+
status: "orphaned",
|
|
206
|
+
note: `the operation started ${last.ts} and the process that owned it (pid ${holder.pid}) is ` +
|
|
207
|
+
"GONE without recording an outcome — its app-side change may or may not have landed. " +
|
|
208
|
+
`Re-read the target (\`things show ${target}\`) and inspect the trace` +
|
|
209
|
+
(tracePath !== null ? ` (${tracePath})` : "") +
|
|
210
|
+
" rather than blind-retrying. Resubmitting the SAME command with the SAME op-id is safe: " +
|
|
211
|
+
"it re-reads state to decide whether that change is there, replaying it rather than " +
|
|
212
|
+
"repeating it (and refusing outright when the record cannot settle the question)",
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
...base,
|
|
217
|
+
status: "intent-only",
|
|
218
|
+
note: "the operation was recorded as STARTED but no final outcome was written, and the record " +
|
|
219
|
+
"names no process, so whether it is still running cannot be determined from here. The " +
|
|
220
|
+
`outcome is UNCERTAIN: re-read the target (\`things show ${target}\`) and inspect the trace` +
|
|
164
221
|
(tracePath !== null ? ` (${tracePath})` : "") +
|
|
165
222
|
" rather than blind-retrying",
|
|
166
223
|
};
|
package/dist/op-result.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"op-result.js","sourceRoot":"","sources":["../src/op-result.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"op-result.js","sourceRoot":"","sources":["../src/op-result.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** The process that owns an in-flight piece of work, as an identity (not just a pid). */
|
|
2
|
+
export interface ProcessInstance {
|
|
3
|
+
pid: number;
|
|
4
|
+
/**
|
|
5
|
+
* The pid's start time (`ps -o lstart=`), stable for that process's whole
|
|
6
|
+
* life. Null when it could not be read — the pid alone then carries the
|
|
7
|
+
* answer, which is weaker but never fabricated.
|
|
8
|
+
*/
|
|
9
|
+
start: string | null;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Does a process with this pid exist right now? (Signal 0 tests existence only.)
|
|
13
|
+
*
|
|
14
|
+
* Non-positive pids are rejected BEFORE the syscall, and that guard is
|
|
15
|
+
* load-bearing rather than defensive tidiness: in `kill(2)`, pid 0 means "every
|
|
16
|
+
* process in the caller's group" and a negative pid means "the group" — both
|
|
17
|
+
* succeed, so a zero or negative holder would report itself alive forever and
|
|
18
|
+
* wedge its idempotency key. A holder is always a real process or it is nothing.
|
|
19
|
+
*/
|
|
20
|
+
export declare function pidAlive(pid: number): boolean;
|
|
21
|
+
/** A pid's start time, or null when the pid is gone / `ps` could not answer. */
|
|
22
|
+
export declare function processStart(pid: number): string | null;
|
|
23
|
+
/** This process, as the identity to record on a piece of work it is starting. */
|
|
24
|
+
export declare function currentInstance(): ProcessInstance;
|
|
25
|
+
/** Seams so tests never shell out. */
|
|
26
|
+
export interface InstanceLivenessDeps {
|
|
27
|
+
pidAlive?: (pid: number) => boolean;
|
|
28
|
+
processStart?: (pid: number) => string | null;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Is the RECORDED instance still running?
|
|
32
|
+
*
|
|
33
|
+
* Deliberately CONSERVATIVE: this answers a question whose wrong answer is
|
|
34
|
+
* expensive in one direction only. Reporting a dead writer as alive costs a
|
|
35
|
+
* caller one more poll; reporting a LIVE writer as dead invites a second
|
|
36
|
+
* execution of a mutation that is still in flight. So every ambiguity — a start
|
|
37
|
+
* time that was never recorded, a `ps` that would not answer now — resolves to
|
|
38
|
+
* "alive". Only a pid that is gone, or one whose start time PROVES it is a
|
|
39
|
+
* different process wearing the same number, is reported dead.
|
|
40
|
+
*/
|
|
41
|
+
export declare function instanceAlive(instance: ProcessInstance, deps?: InstanceLivenessDeps): boolean;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PROCESS INSTANCE IDENTITY — a pid paired with its start time, which is what
|
|
3
|
+
* makes "is that process still alive?" answerable across a pid reuse.
|
|
4
|
+
*
|
|
5
|
+
* A bare pid is not an identity: the kernel recycles pids, so a dead writer's
|
|
6
|
+
* number can belong to an unrelated process minutes later and `kill(pid, 0)`
|
|
7
|
+
* would report the writer as alive. Pairing the pid with `ps -o lstart=` — a
|
|
8
|
+
* value the reusing process cannot reproduce, because it is the moment the
|
|
9
|
+
* kernel started IT — turns the pair into an identity that only the original
|
|
10
|
+
* process satisfies.
|
|
11
|
+
*
|
|
12
|
+
* This is the same instance-key shape `session-grant.ts` uses to decide whether
|
|
13
|
+
* the app instance a grant was minted for is still the one running; both callers
|
|
14
|
+
* share this module so the liveness rule is stated once.
|
|
15
|
+
*/
|
|
16
|
+
import { execFileSync } from "node:child_process";
|
|
17
|
+
/**
|
|
18
|
+
* Does a process with this pid exist right now? (Signal 0 tests existence only.)
|
|
19
|
+
*
|
|
20
|
+
* Non-positive pids are rejected BEFORE the syscall, and that guard is
|
|
21
|
+
* load-bearing rather than defensive tidiness: in `kill(2)`, pid 0 means "every
|
|
22
|
+
* process in the caller's group" and a negative pid means "the group" — both
|
|
23
|
+
* succeed, so a zero or negative holder would report itself alive forever and
|
|
24
|
+
* wedge its idempotency key. A holder is always a real process or it is nothing.
|
|
25
|
+
*/
|
|
26
|
+
export function pidAlive(pid) {
|
|
27
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
28
|
+
return false;
|
|
29
|
+
try {
|
|
30
|
+
process.kill(pid, 0);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** A pid's start time, or null when the pid is gone / `ps` could not answer. */
|
|
38
|
+
export function processStart(pid) {
|
|
39
|
+
try {
|
|
40
|
+
const out = execFileSync("ps", ["-p", String(pid), "-o", "lstart="], {
|
|
41
|
+
encoding: "utf8",
|
|
42
|
+
timeout: 3000,
|
|
43
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
44
|
+
});
|
|
45
|
+
const trimmed = out.trim();
|
|
46
|
+
return trimmed === "" ? null : trimmed;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// A dead pid exits nonzero — the instance is gone, which is an answer.
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** This process, as the identity to record on a piece of work it is starting. */
|
|
54
|
+
export function currentInstance() {
|
|
55
|
+
return { pid: process.pid, start: processStart(process.pid) };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Is the RECORDED instance still running?
|
|
59
|
+
*
|
|
60
|
+
* Deliberately CONSERVATIVE: this answers a question whose wrong answer is
|
|
61
|
+
* expensive in one direction only. Reporting a dead writer as alive costs a
|
|
62
|
+
* caller one more poll; reporting a LIVE writer as dead invites a second
|
|
63
|
+
* execution of a mutation that is still in flight. So every ambiguity — a start
|
|
64
|
+
* time that was never recorded, a `ps` that would not answer now — resolves to
|
|
65
|
+
* "alive". Only a pid that is gone, or one whose start time PROVES it is a
|
|
66
|
+
* different process wearing the same number, is reported dead.
|
|
67
|
+
*/
|
|
68
|
+
export function instanceAlive(instance, deps = {}) {
|
|
69
|
+
const alive = deps.pidAlive ?? pidAlive;
|
|
70
|
+
const start = deps.processStart ?? processStart;
|
|
71
|
+
if (!alive(instance.pid))
|
|
72
|
+
return false;
|
|
73
|
+
// The pid exists. Without both start times we cannot rule out reuse, so we
|
|
74
|
+
// do not: the conservative answer is that this is still our process.
|
|
75
|
+
if (instance.start === null)
|
|
76
|
+
return true;
|
|
77
|
+
const current = start(instance.pid);
|
|
78
|
+
if (current === null)
|
|
79
|
+
return true;
|
|
80
|
+
return current === instance.start;
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=process-instance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"process-instance.js","sourceRoot":"","sources":["../src/process-instance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAalD;;;;;;;;GAQG;AACH,MAAM,UAAU,QAAQ,CAAC,GAAW;IAClC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IACrD,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACrB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,YAAY,CAAC,GAAW;IACtC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,SAAS,CAAC,EAAE;YACnE,QAAQ,EAAE,MAAM;YAChB,OAAO,EAAE,IAAI;YACb,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC;SACpC,CAAC,CAAC;QACH,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;QAC3B,OAAO,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,uEAAuE;QACvE,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,eAAe;IAC7B,OAAO,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,KAAK,EAAE,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;AAChE,CAAC;AAQD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,aAAa,CAAC,QAAyB,EAAE,OAA6B,EAAE;IACtF,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,IAAI,QAAQ,CAAC;IACxC,MAAM,KAAK,GAAG,IAAI,CAAC,YAAY,IAAI,YAAY,CAAC;IAChD,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IACvC,2EAA2E;IAC3E,qEAAqE;IACrE,IAAI,QAAQ,CAAC,KAAK,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACzC,MAAM,OAAO,GAAG,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;IACpC,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAClC,OAAO,OAAO,KAAK,QAAQ,CAAC,KAAK,CAAC;AACpC,CAAC"}
|
package/dist/rescue.d.ts
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `things rescue` — the headless emergency surface for un-sticking Things
|
|
3
|
+
* (issue #640).
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS EXISTS. Things can reach a state where the app is up, answering, and
|
|
6
|
+
* unusable to automation, with no scripted way back. Three measured behaviours
|
|
7
|
+
* converge on it:
|
|
8
|
+
*
|
|
9
|
+
* - a standing modal sheet EMPTIES the app's top-level AppleScript collections
|
|
10
|
+
* (oddities §25 / MODALX1), so `delete` reports `-1728 Can't get to do id` on
|
|
11
|
+
* a row the database holds open — issue #620's "ghost clone", which has no
|
|
12
|
+
* second cause;
|
|
13
|
+
* - a standing sheet GATES Things Cloud sync entirely (oddities §24), so writes
|
|
14
|
+
* land locally and silently never leave the Mac;
|
|
15
|
+
* - a Repeat dialog opened while Things is BACKGROUNDED becomes a detached
|
|
16
|
+
* window that nothing dismisses (oddities §26 / DRVLAT1 §5) — not its own
|
|
17
|
+
* Cancel, not Escape, not ⌘W, not a real HID click at its Cancel frame.
|
|
18
|
+
*
|
|
19
|
+
* The shipped driver never creates the third state (every dialog recipe
|
|
20
|
+
* activates Things first, which is exactly why that step is permanent), but a
|
|
21
|
+
* killed client, a harness tool-timeout or a person walking away mid-dialog can
|
|
22
|
+
* all leave the first two. {@link rescueStatus} reports them; the other two
|
|
23
|
+
* verbs act on them. This is the single home for both halves — the read-only
|
|
24
|
+
* census that was once the top-level `ui-state` command lives here now.
|
|
25
|
+
*
|
|
26
|
+
* THE THREE VERBS, separated by what they can cost you:
|
|
27
|
+
*
|
|
28
|
+
* - {@link rescueStatus} free, ungated, read-only — the census + the lock
|
|
29
|
+
* table + a "we must not touch that" verdict;
|
|
30
|
+
* - {@link rescueDismiss} gated two ways — presses ONE dialog's own Cancel;
|
|
31
|
+
* - {@link rescueRelaunch} gated two ways — ends the process and starts it
|
|
32
|
+
* again, the only cure measured for oddities §26.
|
|
33
|
+
*
|
|
34
|
+
* PERMISSIONS DOCTRINE. Nothing here raises a macOS consent dialog. The screen
|
|
35
|
+
* is read behind the prompt-free capability verdict, exactly as `doctor
|
|
36
|
+
* --ui-state` reads it, and a machine that has not granted the access is TOLD so
|
|
37
|
+
* rather than
|
|
38
|
+
* prompted. {@link rescueRelaunch} is the deliberate exception to needing that
|
|
39
|
+
* grant at all — see its own note.
|
|
40
|
+
*/
|
|
41
|
+
import { type AuditWriter } from "./audit/log.ts";
|
|
42
|
+
import { type UiCapability } from "./capability.ts";
|
|
43
|
+
import { type UiState } from "./write/vectors/ui-state.ts";
|
|
44
|
+
import { type UiRunner } from "./write/vectors/ui.ts";
|
|
45
|
+
/** What a relaunch could establish about the database it left behind. */
|
|
46
|
+
export interface RescueSchemaVerdict {
|
|
47
|
+
/** Did the database open and read as a shape this build understands? */
|
|
48
|
+
ok: boolean;
|
|
49
|
+
/** One sentence, for the report. */
|
|
50
|
+
detail: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Every side-effecting thing this module does, injectable. Production defaults
|
|
54
|
+
* are the real ones; the unit suite substitutes all of them, which is what makes
|
|
55
|
+
* the gating, the refusals and the kill ladder testable without an app.
|
|
56
|
+
*/
|
|
57
|
+
export interface RescueDeps {
|
|
58
|
+
/** The prompt-free GUI capability verdict. */
|
|
59
|
+
uiCapability?: () => UiCapability;
|
|
60
|
+
/** The osascript / reveal dispatch seam — the same transport the drive uses. */
|
|
61
|
+
run?: UiRunner;
|
|
62
|
+
/** Process environment, for config, the lock path and the change-history directory. */
|
|
63
|
+
env?: NodeJS.ProcessEnv;
|
|
64
|
+
/** Where a rescue ACTION records what it did. */
|
|
65
|
+
audit?: AuditWriter;
|
|
66
|
+
now?: () => number;
|
|
67
|
+
pidAlive?: (pid: number) => boolean;
|
|
68
|
+
/** Which pids Things is running as, newest first; empty when it is not running. */
|
|
69
|
+
thingsPids?: () => Promise<number[]>;
|
|
70
|
+
/** Signal one pid. Never throws for a pid that has already gone. */
|
|
71
|
+
signal?: (pid: number, sig: NodeJS.Signals) => void;
|
|
72
|
+
/** Start Things in the BACKGROUND — we do not steal the screen to fix the screen. */
|
|
73
|
+
launch?: () => Promise<{
|
|
74
|
+
ok: boolean;
|
|
75
|
+
detail: string;
|
|
76
|
+
}>;
|
|
77
|
+
/** Read the database's own verdict, for the post-relaunch check. */
|
|
78
|
+
schemaStatus?: () => RescueSchemaVerdict;
|
|
79
|
+
sleep?: (ms: number) => Promise<void>;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* A modal that is NOT ours. `inspectable: false` is the signature of a secure
|
|
83
|
+
* system modal — a macOS privacy/consent dialog, which belongs to no
|
|
84
|
+
* application's Accessibility tree and which no rescue verb may ever press a
|
|
85
|
+
* button on. It is reported so the operator knows to look at the screen, and so
|
|
86
|
+
* they know the refusal that follows is deliberate rather than a failure.
|
|
87
|
+
*/
|
|
88
|
+
export interface RescueForeignModal {
|
|
89
|
+
/** The application that owns the screen, or null when even that could not be read. */
|
|
90
|
+
owner: string | null;
|
|
91
|
+
detail: string;
|
|
92
|
+
}
|
|
93
|
+
/** The mutation lock, as `rescue status` reports it. Read-only, always. */
|
|
94
|
+
export interface RescueLockView {
|
|
95
|
+
path: string;
|
|
96
|
+
held: boolean;
|
|
97
|
+
pid: number | null;
|
|
98
|
+
/** ISO instant the holder took it. */
|
|
99
|
+
since: string | null;
|
|
100
|
+
/** Human age ("4m 12s"), or null when it could not be computed. */
|
|
101
|
+
heldFor: string | null;
|
|
102
|
+
/** Does a process with that pid still exist? */
|
|
103
|
+
alive: boolean;
|
|
104
|
+
/** Alive AND older than any change takes — old enough to say so. */
|
|
105
|
+
suspect: boolean;
|
|
106
|
+
detail: string;
|
|
107
|
+
}
|
|
108
|
+
export interface RescueStatusReport {
|
|
109
|
+
/** Could the screen be read at all on this machine? */
|
|
110
|
+
screenReadable: boolean;
|
|
111
|
+
/** One sentence: the census summary, or the reason there is none. */
|
|
112
|
+
detail: string;
|
|
113
|
+
/** The census; null when it could not be read. */
|
|
114
|
+
state: UiState | null;
|
|
115
|
+
lock: RescueLockView;
|
|
116
|
+
/** Set when something we must not touch owns the screen. */
|
|
117
|
+
foreignModal: RescueForeignModal | null;
|
|
118
|
+
/** Consequences worth stating — the open-dialog sync gate. */
|
|
119
|
+
warnings: string[];
|
|
120
|
+
/** What to do next, including which rescue verb applies. */
|
|
121
|
+
remediation: string[];
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Read everything a stuck machine can be asked about, without touching any of
|
|
125
|
+
* it. Never throws and never refuses: the cases this exists for are exactly the
|
|
126
|
+
* ones where something is already wrong, so every failure is a REPORTED state.
|
|
127
|
+
*/
|
|
128
|
+
export declare function rescueStatus(deps?: RescueDeps): Promise<RescueStatusReport>;
|
|
129
|
+
export type RescueDismissOutcome =
|
|
130
|
+
/** Nothing was stranded — no dialog was open. */
|
|
131
|
+
"no-dialog"
|
|
132
|
+
/** Cancel was pressed and the dialog is confirmed gone. */
|
|
133
|
+
| "dismissed"
|
|
134
|
+
/** Cancel was pressed and the dialog is demonstrably still there (oddities §26). */
|
|
135
|
+
| "still-open"
|
|
136
|
+
/** Cancel was pressed and the screen could not be re-read to confirm either way. */
|
|
137
|
+
| "unverified"
|
|
138
|
+
/** Nothing was pressed. */
|
|
139
|
+
| "refused";
|
|
140
|
+
export interface RescueDismissResult {
|
|
141
|
+
outcome: RescueDismissOutcome;
|
|
142
|
+
/** Which press worked, when one did. */
|
|
143
|
+
how: "cancel-button" | "cancel-click" | null;
|
|
144
|
+
before: UiState | null;
|
|
145
|
+
after: UiState | null;
|
|
146
|
+
/** Dialogs still stacked after this one; null when it could not be re-read. */
|
|
147
|
+
levelsRemaining: number | null;
|
|
148
|
+
detail: string;
|
|
149
|
+
remediation: string[];
|
|
150
|
+
notes: string[];
|
|
151
|
+
warnings: string[];
|
|
152
|
+
}
|
|
153
|
+
/** The two keys `rescue dismiss` needs, spelled the way each surface spells them. */
|
|
154
|
+
export interface RescueDismissAck {
|
|
155
|
+
/** `--dangerously-dismiss-dialog`. */
|
|
156
|
+
dangerouslyDismissDialog?: boolean;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Close the dialog IN FRONT by pressing its own Cancel button — FGRD2's proven
|
|
160
|
+
* Cancel, generalized off the drive's abort path and onto a dialog no drive
|
|
161
|
+
* opened.
|
|
162
|
+
*
|
|
163
|
+
* EXACTLY ONE LIFO LEVEL PER INVOCATION. Sheets nest as `AXSheet` children of
|
|
164
|
+
* the sheet below and dismiss strictly last-in-first-out (MODALX1 §6). A loop
|
|
165
|
+
* that cleared a whole stack in one command would be a loop pressing an unknown
|
|
166
|
+
* number of buttons on an unknown number of dialogs; instead this closes the top
|
|
167
|
+
* one, re-reads, and reports how many remain so the caller decides.
|
|
168
|
+
*
|
|
169
|
+
* PROVEN, never assumed. The press reporting success is NOT closure: the
|
|
170
|
+
* detached editor of oddities §26 accepts an AXPress on its Cancel and stays
|
|
171
|
+
* exactly where it is. Closure is decided by a fresh census showing the stack one
|
|
172
|
+
* level shorter, and an unconfirmable press is reported as unconfirmed.
|
|
173
|
+
*
|
|
174
|
+
* WHAT IT CAN COST YOU: the dialog's own pending edits, and nothing else. The
|
|
175
|
+
* database is untouched, and a URL command parked behind a consent sheet is
|
|
176
|
+
* discarded with it (URLEN1).
|
|
177
|
+
*/
|
|
178
|
+
export declare function rescueDismiss(ack?: RescueDismissAck, deps?: RescueDeps): Promise<RescueDismissResult>;
|
|
179
|
+
export type RescueRelaunchOutcome = "relaunched" | "refused" | "failed";
|
|
180
|
+
export interface RescueRelaunchResult {
|
|
181
|
+
outcome: RescueRelaunchOutcome;
|
|
182
|
+
/** Which rung actually ended the process. */
|
|
183
|
+
endedBy: "quit" | "sigterm" | "sigkill" | "not-running" | null;
|
|
184
|
+
before: UiState | null;
|
|
185
|
+
after: UiState | null;
|
|
186
|
+
/** The step-by-step account of the ladder, always present. */
|
|
187
|
+
ladder: string[];
|
|
188
|
+
detail: string;
|
|
189
|
+
remediation: string[];
|
|
190
|
+
notes: string[];
|
|
191
|
+
warnings: string[];
|
|
192
|
+
}
|
|
193
|
+
/** The keys `rescue relaunch` needs. The second one is profile-dependent. */
|
|
194
|
+
export interface RescueRelaunchAck {
|
|
195
|
+
/** `--yes`. */
|
|
196
|
+
yes?: boolean;
|
|
197
|
+
/** `--dangerously-force-quit`; required only under the `workstation` profile. */
|
|
198
|
+
dangerouslyForceQuit?: boolean;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* End Things and start it again — the nuclear rung, and the ONLY cure this
|
|
202
|
+
* project has measured for the detached editor of oddities §26. DRVLAT1 §5 tried
|
|
203
|
+
* every other route against the same live dialog (its own Cancel backgrounded,
|
|
204
|
+
* foregrounded and after `AXRaise`; Escape; ⌘W then re-activate; a real HID click
|
|
205
|
+
* at the AX-resolved Cancel frame) and every one was inert.
|
|
206
|
+
*
|
|
207
|
+
* THE LADDER, each rung bounded:
|
|
208
|
+
*
|
|
209
|
+
* 1. ask Things to quit through its own scripting interface. Bounded, because
|
|
210
|
+
* a standing sheet swallows ⌘Q and the `quit` verb can block behind one.
|
|
211
|
+
* 2. SIGTERM, and wait for the process to go.
|
|
212
|
+
* 3. SIGKILL — the lab's canonical reset for a modal that will not clear.
|
|
213
|
+
* 4. start it again in the BACKGROUND: we do not steal the screen to fix the
|
|
214
|
+
* screen.
|
|
215
|
+
* 5. prove it: the process is answering, the database still reads as the shape
|
|
216
|
+
* this version expects, and no dialog is open.
|
|
217
|
+
*
|
|
218
|
+
* WHAT IS LOST: the Things database is write-ahead-logged, so everything already
|
|
219
|
+
* committed survives a kill unconditionally. The ceiling is what was never
|
|
220
|
+
* committed — the edits sitting in the dialog being destroyed.
|
|
221
|
+
*
|
|
222
|
+
* WHAT COMES BACK: sync. While a dialog stands, Things stops sending changes to
|
|
223
|
+
* Things Cloud (oddities §24); killing the dialog with the process releases that,
|
|
224
|
+
* and everything written on this Mac in the meantime goes out.
|
|
225
|
+
*
|
|
226
|
+
* DELIBERATELY NOT GATED ON THE GUI CAPABILITY. Every other verb that touches the
|
|
227
|
+
* app needs the GUI grant, because it synthesizes input into someone's window.
|
|
228
|
+
* This one signals a process the user already owns, which needs no grant at all —
|
|
229
|
+
* and the machines that need it most are exactly the ones where the Accessibility
|
|
230
|
+
* path is what broke. The quit rung and the census still run through the granted
|
|
231
|
+
* seam when it is available, and are skipped without complaint when it is not.
|
|
232
|
+
*/
|
|
233
|
+
export declare function rescueRelaunch(ack?: RescueRelaunchAck, deps?: RescueDeps): Promise<RescueRelaunchResult>;
|
|
234
|
+
/**
|
|
235
|
+
* The human render of `things rescue status`.
|
|
236
|
+
*
|
|
237
|
+
* A ROW WHOSE PROBE DID NOT ANSWER SAYS SO (issue #629). Printing a field's
|
|
238
|
+
* unset default ("none", "unknown") beside rows that WERE measured is exactly
|
|
239
|
+
* what made a stalled inspection read as a clean screen in the field, so an
|
|
240
|
+
* unproven probe renders as "not established" rather than as its default, and
|
|
241
|
+
* the probes are named again in full on the `unproven:` row.
|
|
242
|
+
*/
|
|
243
|
+
export declare function rescueStatusLines(report: RescueStatusReport): string[];
|
|
244
|
+
/** The human render of `things rescue dismiss`. */
|
|
245
|
+
export declare function rescueDismissLines(result: RescueDismissResult): string[];
|
|
246
|
+
/** The human render of `things rescue relaunch`. */
|
|
247
|
+
export declare function rescueRelaunchLines(result: RescueRelaunchResult): string[];
|