@agimon-ai/po-config 0.0.0-alpha.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/LICENSE +21 -0
- package/README.md +170 -0
- package/dist/agentPluginMcp.cjs +322 -0
- package/dist/agentPluginMcp.cjs.map +1 -0
- package/dist/agentPluginMcp.d.mts +38 -0
- package/dist/agentPluginMcp.d.mts.map +1 -0
- package/dist/agentPluginMcp.mjs +318 -0
- package/dist/agentPluginMcp.mjs.map +1 -0
- package/dist/apiContracts.cjs +203 -0
- package/dist/apiContracts.cjs.map +1 -0
- package/dist/apiContracts.d.mts +53 -0
- package/dist/apiContracts.d.mts.map +1 -0
- package/dist/apiContracts.mjs +198 -0
- package/dist/apiContracts.mjs.map +1 -0
- package/dist/atomicJson-1W0WhuFF.mjs +44 -0
- package/dist/atomicJson-1W0WhuFF.mjs.map +1 -0
- package/dist/atomicJson-Dt2z_DXl.cjs +52 -0
- package/dist/atomicJson-Dt2z_DXl.cjs.map +1 -0
- package/dist/atomicJson.cjs +3 -0
- package/dist/atomicJson.d.mts +2 -0
- package/dist/atomicJson.mjs +2 -0
- package/dist/config-BUw5kHsx.cjs +219 -0
- package/dist/config-BUw5kHsx.cjs.map +1 -0
- package/dist/config-CKA07FxG.mjs +162 -0
- package/dist/config-CKA07FxG.mjs.map +1 -0
- package/dist/config-Cm3EV1jp.d.mts +320 -0
- package/dist/config-Cm3EV1jp.d.mts.map +1 -0
- package/dist/config-NSHRd6na.mjs +2 -0
- package/dist/config-zZz80Mh5.cjs +1 -0
- package/dist/config.cjs +24 -0
- package/dist/config.d.mts +3 -0
- package/dist/config.mjs +3 -0
- package/dist/configPolicy--TNX7feH.mjs +718 -0
- package/dist/configPolicy--TNX7feH.mjs.map +1 -0
- package/dist/configPolicy-DmsZEp0t.cjs +795 -0
- package/dist/configPolicy-DmsZEp0t.cjs.map +1 -0
- package/dist/configSchema.cjs +0 -0
- package/dist/configSchema.d.mts +2 -0
- package/dist/configSchema.mjs +1 -0
- package/dist/configWriter-BTuinLjs.cjs +150 -0
- package/dist/configWriter-BTuinLjs.cjs.map +1 -0
- package/dist/configWriter-BgkGaLrf.mjs +130 -0
- package/dist/configWriter-BgkGaLrf.mjs.map +1 -0
- package/dist/configWriter.cjs +5 -0
- package/dist/configWriter.d.mts +2 -0
- package/dist/configWriter.mjs +2 -0
- package/dist/domains-CBYkuxs5.cjs +727 -0
- package/dist/domains-CBYkuxs5.cjs.map +1 -0
- package/dist/domains-DZOu9vYM.mjs +633 -0
- package/dist/domains-DZOu9vYM.mjs.map +1 -0
- package/dist/domains.cjs +19 -0
- package/dist/domains.d.mts +3 -0
- package/dist/domains.mjs +3 -0
- package/dist/extensions/mcp-ui.mjs +101 -0
- package/dist/extensions/mcp-ui.mjs.map +1 -0
- package/dist/extensions/mcp.cjs +171 -0
- package/dist/extensions/mcp.cjs.map +1 -0
- package/dist/extensions/mcp.d.mts +5 -0
- package/dist/extensions/mcp.d.mts.map +1 -0
- package/dist/extensions/mcp.mjs +166 -0
- package/dist/extensions/mcp.mjs.map +1 -0
- package/dist/extensions/server.cjs +504 -0
- package/dist/extensions/server.cjs.map +1 -0
- package/dist/extensions/server.d.mts +5 -0
- package/dist/extensions/server.d.mts.map +1 -0
- package/dist/extensions/server.mjs +499 -0
- package/dist/extensions/server.mjs.map +1 -0
- package/dist/harnessState-CCyBeutB.cjs +188 -0
- package/dist/harnessState-CCyBeutB.cjs.map +1 -0
- package/dist/harnessState-Dn_S7fB9.mjs +163 -0
- package/dist/harnessState-Dn_S7fB9.mjs.map +1 -0
- package/dist/harnessState.cjs +6 -0
- package/dist/harnessState.d.mts +2 -0
- package/dist/harnessState.mjs +2 -0
- package/dist/harnessStore-CJSxdSkV.cjs +343 -0
- package/dist/harnessStore-CJSxdSkV.cjs.map +1 -0
- package/dist/harnessStore-CltwLB7M.mjs +262 -0
- package/dist/harnessStore-CltwLB7M.mjs.map +1 -0
- package/dist/harnessStore.cjs +15 -0
- package/dist/harnessStore.d.mts +2 -0
- package/dist/harnessStore.mjs +2 -0
- package/dist/index-BAaTySaM.d.mts +35 -0
- package/dist/index-BAaTySaM.d.mts.map +1 -0
- package/dist/index-BT29GZx5.d.mts +142 -0
- package/dist/index-BT29GZx5.d.mts.map +1 -0
- package/dist/index-B_lp0W3U.d.mts +114 -0
- package/dist/index-B_lp0W3U.d.mts.map +1 -0
- package/dist/index-C6GaYjq-.d.mts +6 -0
- package/dist/index-C6GaYjq-.d.mts.map +1 -0
- package/dist/index-CIQrKS1A.d.mts +50 -0
- package/dist/index-CIQrKS1A.d.mts.map +1 -0
- package/dist/index-CRcoVYXz.d.mts +39 -0
- package/dist/index-CRcoVYXz.d.mts.map +1 -0
- package/dist/index-CVlciCJF.d.mts +40 -0
- package/dist/index-CVlciCJF.d.mts.map +1 -0
- package/dist/index-CeOk2kqs.d.mts +118 -0
- package/dist/index-CeOk2kqs.d.mts.map +1 -0
- package/dist/index-Ch7ptOkW.d.mts +54 -0
- package/dist/index-Ch7ptOkW.d.mts.map +1 -0
- package/dist/index-Cvdt-Y-i.d.mts +66 -0
- package/dist/index-Cvdt-Y-i.d.mts.map +1 -0
- package/dist/index-DJpG-I9K.d.mts +122 -0
- package/dist/index-DJpG-I9K.d.mts.map +1 -0
- package/dist/index-DNk4q0Qm.d.mts +81 -0
- package/dist/index-DNk4q0Qm.d.mts.map +1 -0
- package/dist/index.cjs +121 -0
- package/dist/index.d.mts +15 -0
- package/dist/index.mjs +15 -0
- package/dist/init-C_9nyp3-.cjs +448 -0
- package/dist/init-C_9nyp3-.cjs.map +1 -0
- package/dist/init-DJ6wm-dr.mjs +415 -0
- package/dist/init-DJ6wm-dr.mjs.map +1 -0
- package/dist/init.cjs +7 -0
- package/dist/init.d.mts +2 -0
- package/dist/init.mjs +2 -0
- package/dist/layeredConfig-B922ibgn.mjs +67 -0
- package/dist/layeredConfig-B922ibgn.mjs.map +1 -0
- package/dist/layeredConfig-DdI5ysUf.cjs +94 -0
- package/dist/layeredConfig-DdI5ysUf.cjs.map +1 -0
- package/dist/layeredConfig.cjs +6 -0
- package/dist/layeredConfig.d.mts +2 -0
- package/dist/layeredConfig.mjs +2 -0
- package/dist/majorModes-CmPLEpMT.mjs +333 -0
- package/dist/majorModes-CmPLEpMT.mjs.map +1 -0
- package/dist/majorModes-DPhVMuhT.cjs +383 -0
- package/dist/majorModes-DPhVMuhT.cjs.map +1 -0
- package/dist/majorModes.cjs +10 -0
- package/dist/majorModes.d.mts +2 -0
- package/dist/majorModes.mjs +2 -0
- package/dist/piConfig-Bx26HMb_.d.mts +33 -0
- package/dist/piConfig-Bx26HMb_.d.mts.map +1 -0
- package/dist/piConfig-CCBdZXQr.cjs +260 -0
- package/dist/piConfig-CCBdZXQr.cjs.map +1 -0
- package/dist/piConfig-qGyCbcVj.mjs +197 -0
- package/dist/piConfig-qGyCbcVj.mjs.map +1 -0
- package/dist/piConfig.cjs +12 -0
- package/dist/piConfig.d.mts +2 -0
- package/dist/piConfig.mjs +2 -0
- package/dist/piContext.cjs +18 -0
- package/dist/piContext.d.mts +3 -0
- package/dist/piContext.mjs +2 -0
- package/dist/profiles-CTrUCmIh.d.mts +27 -0
- package/dist/profiles-CTrUCmIh.d.mts.map +1 -0
- package/dist/profiles-DnF9DBpd.cjs +552 -0
- package/dist/profiles-DnF9DBpd.cjs.map +1 -0
- package/dist/profiles-fujQKrS0.mjs +483 -0
- package/dist/profiles-fujQKrS0.mjs.map +1 -0
- package/dist/profiles.cjs +13 -0
- package/dist/profiles.d.mts +25 -0
- package/dist/profiles.d.mts.map +1 -0
- package/dist/profiles.mjs +2 -0
- package/dist/rolldown-runtime-VH7oDXx4.cjs +28 -0
- package/dist/selectionSwitch.cjs +70 -0
- package/dist/selectionSwitch.cjs.map +1 -0
- package/dist/selectionSwitch.d.mts +30 -0
- package/dist/selectionSwitch.d.mts.map +1 -0
- package/dist/selectionSwitch.mjs +64 -0
- package/dist/selectionSwitch.mjs.map +1 -0
- package/dist/sessionConfig-Boh4Fdje.cjs +342 -0
- package/dist/sessionConfig-Boh4Fdje.cjs.map +1 -0
- package/dist/sessionConfig-CtLoc-lO.mjs +240 -0
- package/dist/sessionConfig-CtLoc-lO.mjs.map +1 -0
- package/dist/types.cjs +9 -0
- package/dist/types.d.mts +2 -0
- package/dist/types.mjs +2 -0
- package/llms.txt +12 -0
- package/package.json +206 -0
- package/src/prompts/po-author-config/SKILL.md +26 -0
- package/src/prompts/po-author-config/agents/openai.yaml +4 -0
- package/src/prompts/po-author-config/references/config-contract.md +201 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
const require_rolldown_runtime = require("./rolldown-runtime-VH7oDXx4.cjs");
|
|
2
|
+
const require_atomicJson = require("./atomicJson-Dt2z_DXl.cjs");
|
|
3
|
+
const require_harnessState = require("./harnessState-CCyBeutB.cjs");
|
|
4
|
+
let node_fs = require("node:fs");
|
|
5
|
+
node_fs = require_rolldown_runtime.__toESM(node_fs, 1);
|
|
6
|
+
let node_path = require("node:path");
|
|
7
|
+
node_path = require_rolldown_runtime.__toESM(node_path, 1);
|
|
8
|
+
let node_os = require("node:os");
|
|
9
|
+
node_os = require_rolldown_runtime.__toESM(node_os, 1);
|
|
10
|
+
let _agimon_ai_po_core_cordisHost = require("@agimon-ai/po-core/cordisHost");
|
|
11
|
+
//#region src/models/harnessCache.ts
|
|
12
|
+
const harnessCache = { loaded: void 0 };
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/services/harnessStore/index.ts
|
|
15
|
+
/**
|
|
16
|
+
* Who owns the harness state, and where it lives.
|
|
17
|
+
*
|
|
18
|
+
* The state used to be the environment: twenty variables that every reader
|
|
19
|
+
* re-parsed and every writer mutated globally. The file is the authority now,
|
|
20
|
+
* and the environment is a projection published for the readers that can only
|
|
21
|
+
* see an environment, which are bash hooks, the shell launchers, packages that
|
|
22
|
+
* do not depend on this one, and any process spawned by any of them.
|
|
23
|
+
*
|
|
24
|
+
* OWNERSHIP IS PER PROCESS, AND THAT IS THE WHOLE DESIGN:
|
|
25
|
+
* Doom Team subagents run detached, in their own process group, from an
|
|
26
|
+
* environment snapshot taken at spawn. A child can outlive its parent. If it
|
|
27
|
+
* shared the parent's file it would corrupt a session it does not own, and if
|
|
28
|
+
* that file sat in the parent's run directory the parent's shutdown sweep would
|
|
29
|
+
* delete it from under a live run. So every process writes a file it owns:
|
|
30
|
+
* `owner.pid` records who, and a process that finds someone else's file copies
|
|
31
|
+
* it before its first write.
|
|
32
|
+
*
|
|
33
|
+
* AVOID:
|
|
34
|
+
* - Treating the file as shared mutable state between processes. Atomic writes
|
|
35
|
+
* prevent torn reads, not lost updates
|
|
36
|
+
* - Reading the environment directly for anything this exposes
|
|
37
|
+
*/
|
|
38
|
+
/** Points at the state file. The one variable that is not derived. */
|
|
39
|
+
const HARNESS_STATE_POINTER = "PO_STATE";
|
|
40
|
+
const STATE_FILE_VERSION = 1;
|
|
41
|
+
const STATE_FILE_NAME = "harness-state.json";
|
|
42
|
+
const ADOPTED_DIRECTORY = "doom-harness";
|
|
43
|
+
/**
|
|
44
|
+
* Written for a process that does not exist yet, so the first one to read it
|
|
45
|
+
* owns it. A spawner cannot know its child's pid, and a child that inherited
|
|
46
|
+
* its parent's file would lose it the moment the parent cleaned up.
|
|
47
|
+
*/
|
|
48
|
+
const UNCLAIMED_OWNER = 0;
|
|
49
|
+
/** Drops the cached state. For tests, and for a process changing identity. */
|
|
50
|
+
function resetHarnessStore() {
|
|
51
|
+
harnessCache.loaded = void 0;
|
|
52
|
+
}
|
|
53
|
+
function fileMtime(filePath) {
|
|
54
|
+
try {
|
|
55
|
+
return node_fs.default.statSync(filePath).mtimeMs;
|
|
56
|
+
} catch {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Whether the cache still describes what the process points at.
|
|
62
|
+
*
|
|
63
|
+
* A session can load more than one copy of this package (a layer package
|
|
64
|
+
* installed under .pi/npm brings its own), and each copy caches on its own.
|
|
65
|
+
* The pointer and the file are shared, so a switch persisted through one copy
|
|
66
|
+
* moves the pointer or rewrites the file, and every other copy must notice
|
|
67
|
+
* rather than answer from memory.
|
|
68
|
+
*/
|
|
69
|
+
function cacheCurrent(cache, environment) {
|
|
70
|
+
const pointer = environment[HARNESS_STATE_POINTER];
|
|
71
|
+
if (cache.filePath !== pointer) return false;
|
|
72
|
+
if (!cache.filePath) return true;
|
|
73
|
+
try {
|
|
74
|
+
return node_fs.default.statSync(cache.filePath).mtimeMs === cache.mtimeMs;
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function parseStateFile(source, filePath) {
|
|
80
|
+
const parsed = JSON.parse(source);
|
|
81
|
+
if (parsed.version !== STATE_FILE_VERSION) throw new Error(`Harness state at ${filePath} has version ${String(parsed.version)}, expected ${STATE_FILE_VERSION}`);
|
|
82
|
+
if (typeof parsed.state !== "object" || parsed.state === null) throw new Error(`Harness state at ${filePath} has no state object`);
|
|
83
|
+
return {
|
|
84
|
+
version: STATE_FILE_VERSION,
|
|
85
|
+
owner: {
|
|
86
|
+
pid: parsed.owner?.pid ?? -1,
|
|
87
|
+
startedAt: parsed.owner?.startedAt ?? ""
|
|
88
|
+
},
|
|
89
|
+
state: parsed.state
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Reads the state for this process, once.
|
|
94
|
+
*
|
|
95
|
+
* A missing pointer means nobody wrote a file for this process, so the
|
|
96
|
+
* environment is all there is: a nested run, a third-party spawn, or a caller
|
|
97
|
+
* older than the store. A pointer that cannot be read is reported and then
|
|
98
|
+
* treated the same way, because a session that starts degraded beats a session
|
|
99
|
+
* that does not start at all.
|
|
100
|
+
*/
|
|
101
|
+
function loadHarnessState(environment = process.env, report) {
|
|
102
|
+
const cacheable = environment === process.env;
|
|
103
|
+
if (cacheable && harnessCache.loaded && cacheCurrent(harnessCache.loaded, environment)) return harnessCache.loaded;
|
|
104
|
+
const filePath = environment[HARNESS_STATE_POINTER];
|
|
105
|
+
let result;
|
|
106
|
+
if (filePath && node_fs.default.existsSync(filePath)) try {
|
|
107
|
+
const file = parseStateFile(node_fs.default.readFileSync(filePath, "utf8"), filePath);
|
|
108
|
+
result = {
|
|
109
|
+
filePath,
|
|
110
|
+
owned: file.owner.pid === process.pid || file.owner.pid === UNCLAIMED_OWNER,
|
|
111
|
+
state: file.state,
|
|
112
|
+
mtimeMs: fileMtime(filePath)
|
|
113
|
+
};
|
|
114
|
+
} catch (error) {
|
|
115
|
+
report?.(HARNESS_STATE_POINTER, error);
|
|
116
|
+
}
|
|
117
|
+
result ??= {
|
|
118
|
+
owned: false,
|
|
119
|
+
state: require_harnessState.readHarnessState(environment, report)
|
|
120
|
+
};
|
|
121
|
+
if (cacheable) harnessCache.loaded = result;
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
/** Captures the owned state pointer and every projected harness variable. */
|
|
125
|
+
function snapshotHarnessState(environment = process.env) {
|
|
126
|
+
const loadedState = loadHarnessState(environment);
|
|
127
|
+
const environmentSnapshot = { [HARNESS_STATE_POINTER]: environment[HARNESS_STATE_POINTER] };
|
|
128
|
+
for (const key of Object.values(require_harnessState.HARNESS_STATE_KEYS)) environmentSnapshot[key] = environment[key];
|
|
129
|
+
return {
|
|
130
|
+
filePath: loadedState.filePath,
|
|
131
|
+
owned: loadedState.owned,
|
|
132
|
+
state: structuredClone(loadedState.state),
|
|
133
|
+
environment: environmentSnapshot
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/** Restores a snapshot only while the current process still owns its state pointer. */
|
|
137
|
+
function restoreHarnessStateSnapshot(snapshot, environment = process.env) {
|
|
138
|
+
const current = loadHarnessState(environment);
|
|
139
|
+
if (snapshot.filePath && snapshot.owned) {
|
|
140
|
+
if (!current.owned || current.filePath !== snapshot.filePath) throw new Error("Harness state ownership changed; refusing to restore an older snapshot.");
|
|
141
|
+
persist(snapshot.filePath, snapshot.state);
|
|
142
|
+
} else if (!snapshot.filePath) {
|
|
143
|
+
if (current.filePath && !current.owned) throw new Error("Harness state is no longer owned by this process; refusing to restore an older snapshot.");
|
|
144
|
+
if (current.filePath && current.owned) node_fs.default.rmSync(current.filePath, { force: true });
|
|
145
|
+
resetHarnessStore();
|
|
146
|
+
} else if (current.filePath !== snapshot.filePath) throw new Error("Harness state pointer changed; refusing to restore an older snapshot.");
|
|
147
|
+
for (const [key, value] of Object.entries(snapshot.environment)) if (value === void 0) delete environment[key];
|
|
148
|
+
else environment[key] = value;
|
|
149
|
+
if (environment === process.env) harnessCache.loaded = snapshot.filePath ? {
|
|
150
|
+
filePath: snapshot.filePath,
|
|
151
|
+
owned: snapshot.owned,
|
|
152
|
+
state: snapshot.state,
|
|
153
|
+
mtimeMs: fileMtime(snapshot.filePath)
|
|
154
|
+
} : void 0;
|
|
155
|
+
}
|
|
156
|
+
function getHarnessState(report) {
|
|
157
|
+
return loadHarnessState(process.env, report).state;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* The harness state of one session, by id.
|
|
161
|
+
*
|
|
162
|
+
* A server runs many sessions' extensions in one process, each with its own state file, so
|
|
163
|
+
* a session-scoped reader asks by session id. A terminal session owns its process, and the
|
|
164
|
+
* answer is the same as `getHarnessState`.
|
|
165
|
+
*/
|
|
166
|
+
function getSessionHarnessState(sessionId, report) {
|
|
167
|
+
return loadHarnessState((0, _agimon_ai_po_core_cordisHost.readDoomSessionEnvironment)(sessionId), report).state;
|
|
168
|
+
}
|
|
169
|
+
function stateFilePath(directory) {
|
|
170
|
+
return node_path.default.join(directory, STATE_FILE_NAME);
|
|
171
|
+
}
|
|
172
|
+
/** Where a process writes state when it was not given a file of its own. */
|
|
173
|
+
function adoptedStateFilePath() {
|
|
174
|
+
return node_path.default.join(node_os.default.tmpdir(), ADOPTED_DIRECTORY, `${String(process.pid)}.json`);
|
|
175
|
+
}
|
|
176
|
+
function persist(filePath, state, ownerPid = process.pid) {
|
|
177
|
+
require_atomicJson.writePrivateAtomicJson(filePath, {
|
|
178
|
+
version: STATE_FILE_VERSION,
|
|
179
|
+
owner: {
|
|
180
|
+
pid: ownerPid,
|
|
181
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
182
|
+
},
|
|
183
|
+
state
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Starts a session: writes the file and publishes the derived environment.
|
|
188
|
+
*
|
|
189
|
+
* The environment is a parameter because the caller is usually describing a
|
|
190
|
+
* process other than itself. The launcher builds the environment for the Pi it
|
|
191
|
+
* is about to spawn, and Doom Team builds the snapshot for a detached child, so
|
|
192
|
+
* neither should touch its own `process.env` to do it.
|
|
193
|
+
*/
|
|
194
|
+
function createHarnessSession(state, options) {
|
|
195
|
+
const filePath = stateFilePath(options.directory);
|
|
196
|
+
persist(filePath, state, options.unclaimed ? UNCLAIMED_OWNER : process.pid);
|
|
197
|
+
options.environment[HARNESS_STATE_POINTER] = filePath;
|
|
198
|
+
require_harnessState.projectHarnessEnvironment(state, options.environment);
|
|
199
|
+
if (!options.unclaimed && options.environment === process.env) harnessCache.loaded = {
|
|
200
|
+
filePath,
|
|
201
|
+
owned: true,
|
|
202
|
+
state,
|
|
203
|
+
mtimeMs: fileMtime(filePath)
|
|
204
|
+
};
|
|
205
|
+
return filePath;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Applies a patch to this process's state.
|
|
209
|
+
*
|
|
210
|
+
* Copy on write: a process holding a file it does not own writes its own copy
|
|
211
|
+
* and repoints itself at it, so a child can never rewrite its parent's session.
|
|
212
|
+
*/
|
|
213
|
+
function updateHarnessState(patch, environment = process.env) {
|
|
214
|
+
const current = loadHarnessState(environment);
|
|
215
|
+
const state = {
|
|
216
|
+
...current.state,
|
|
217
|
+
...patch
|
|
218
|
+
};
|
|
219
|
+
const filePath = current.owned && current.filePath ? current.filePath : adoptedStateFilePath();
|
|
220
|
+
persist(filePath, state);
|
|
221
|
+
environment[HARNESS_STATE_POINTER] = filePath;
|
|
222
|
+
if (environment === process.env) harnessCache.loaded = {
|
|
223
|
+
filePath,
|
|
224
|
+
owned: true,
|
|
225
|
+
state,
|
|
226
|
+
mtimeMs: fileMtime(filePath)
|
|
227
|
+
};
|
|
228
|
+
require_harnessState.projectHarnessEnvironment(patch, environment);
|
|
229
|
+
return state;
|
|
230
|
+
}
|
|
231
|
+
/** Removes this process's own state file. Safe when there is none. */
|
|
232
|
+
function disposeHarnessState(environment = process.env) {
|
|
233
|
+
const current = harnessCache.loaded;
|
|
234
|
+
resetHarnessStore();
|
|
235
|
+
if (!current?.owned || !current.filePath) return;
|
|
236
|
+
node_fs.default.rmSync(current.filePath, { force: true });
|
|
237
|
+
delete environment[HARNESS_STATE_POINTER];
|
|
238
|
+
}
|
|
239
|
+
function harnessRoot(state = getHarnessState()) {
|
|
240
|
+
return state.root ?? process.cwd();
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* The repository root, or a hard error.
|
|
244
|
+
*
|
|
245
|
+
* Separate from `requireHarnessPaths` because most callers only need the root,
|
|
246
|
+
* and separate from `harnessRoot` because falling back to `process.cwd()` would
|
|
247
|
+
* quietly operate on whatever directory the session happens to sit in. A caller
|
|
248
|
+
* that asks for the root wants the session's repository or nothing.
|
|
249
|
+
*/
|
|
250
|
+
function requireHarnessRoot(state = getHarnessState()) {
|
|
251
|
+
if (!state.root) throw new Error(`${require_harnessState.HARNESS_STATE_KEYS.root} is not set`);
|
|
252
|
+
return state.root;
|
|
253
|
+
}
|
|
254
|
+
/** The root and the run's scratch directory, for callers that write to disk. */
|
|
255
|
+
function requireHarnessPaths(state = getHarnessState()) {
|
|
256
|
+
const root = requireHarnessRoot(state);
|
|
257
|
+
if (!state.temporaryDirectory) throw new Error(`${require_harnessState.HARNESS_STATE_KEYS.temporaryDirectory} is not set`);
|
|
258
|
+
return {
|
|
259
|
+
root,
|
|
260
|
+
temporaryDirectory: state.temporaryDirectory
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
//#endregion
|
|
264
|
+
Object.defineProperty(exports, "HARNESS_STATE_POINTER", {
|
|
265
|
+
enumerable: true,
|
|
266
|
+
get: function() {
|
|
267
|
+
return HARNESS_STATE_POINTER;
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
Object.defineProperty(exports, "createHarnessSession", {
|
|
271
|
+
enumerable: true,
|
|
272
|
+
get: function() {
|
|
273
|
+
return createHarnessSession;
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
Object.defineProperty(exports, "disposeHarnessState", {
|
|
277
|
+
enumerable: true,
|
|
278
|
+
get: function() {
|
|
279
|
+
return disposeHarnessState;
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
Object.defineProperty(exports, "getHarnessState", {
|
|
283
|
+
enumerable: true,
|
|
284
|
+
get: function() {
|
|
285
|
+
return getHarnessState;
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
Object.defineProperty(exports, "getSessionHarnessState", {
|
|
289
|
+
enumerable: true,
|
|
290
|
+
get: function() {
|
|
291
|
+
return getSessionHarnessState;
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
Object.defineProperty(exports, "harnessRoot", {
|
|
295
|
+
enumerable: true,
|
|
296
|
+
get: function() {
|
|
297
|
+
return harnessRoot;
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
Object.defineProperty(exports, "loadHarnessState", {
|
|
301
|
+
enumerable: true,
|
|
302
|
+
get: function() {
|
|
303
|
+
return loadHarnessState;
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
Object.defineProperty(exports, "requireHarnessPaths", {
|
|
307
|
+
enumerable: true,
|
|
308
|
+
get: function() {
|
|
309
|
+
return requireHarnessPaths;
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
Object.defineProperty(exports, "requireHarnessRoot", {
|
|
313
|
+
enumerable: true,
|
|
314
|
+
get: function() {
|
|
315
|
+
return requireHarnessRoot;
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
Object.defineProperty(exports, "resetHarnessStore", {
|
|
319
|
+
enumerable: true,
|
|
320
|
+
get: function() {
|
|
321
|
+
return resetHarnessStore;
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
Object.defineProperty(exports, "restoreHarnessStateSnapshot", {
|
|
325
|
+
enumerable: true,
|
|
326
|
+
get: function() {
|
|
327
|
+
return restoreHarnessStateSnapshot;
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
Object.defineProperty(exports, "snapshotHarnessState", {
|
|
331
|
+
enumerable: true,
|
|
332
|
+
get: function() {
|
|
333
|
+
return snapshotHarnessState;
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
Object.defineProperty(exports, "updateHarnessState", {
|
|
337
|
+
enumerable: true,
|
|
338
|
+
get: function() {
|
|
339
|
+
return updateHarnessState;
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
//# sourceMappingURL=harnessStore-CJSxdSkV.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"harnessStore-CJSxdSkV.cjs","names":["fs","readHarnessState","HARNESS_STATE_KEYS","readDoomSessionEnvironment","path","os"],"sources":["../src/models/harnessCache.ts","../src/services/harnessStore/index.ts"],"sourcesContent":["import type { HarnessState } from '../types/config';\n\nexport interface LoadedHarnessState {\n /** Undefined when this process is running off the environment fallback. */\n filePath?: string;\n owned: boolean;\n state: HarnessState;\n}\n\nexport interface CachedHarnessState extends LoadedHarnessState {\n /** The file's mtime when it was read, so a rewrite by another copy is noticed. */\n mtimeMs?: number;\n}\n\nexport const harnessCache: { loaded: CachedHarnessState | undefined } = { loaded: undefined };\n","import fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\n\nimport { readDoomSessionEnvironment } from '@agimon-ai/po-core/cordisHost';\n\nimport { harnessCache, type CachedHarnessState, type LoadedHarnessState } from '../../models/harnessCache';\nimport type { HarnessState } from '../../types/config';\nimport { writePrivateAtomicJson } from '../atomicJson';\nimport {\n HARNESS_STATE_KEYS,\n projectHarnessEnvironment,\n readHarnessState,\n type HarnessStateParseReporter,\n} from '../harnessState';\n\n/**\n * Who owns the harness state, and where it lives.\n *\n * The state used to be the environment: twenty variables that every reader\n * re-parsed and every writer mutated globally. The file is the authority now,\n * and the environment is a projection published for the readers that can only\n * see an environment, which are bash hooks, the shell launchers, packages that\n * do not depend on this one, and any process spawned by any of them.\n *\n * OWNERSHIP IS PER PROCESS, AND THAT IS THE WHOLE DESIGN:\n * Doom Team subagents run detached, in their own process group, from an\n * environment snapshot taken at spawn. A child can outlive its parent. If it\n * shared the parent's file it would corrupt a session it does not own, and if\n * that file sat in the parent's run directory the parent's shutdown sweep would\n * delete it from under a live run. So every process writes a file it owns:\n * `owner.pid` records who, and a process that finds someone else's file copies\n * it before its first write.\n *\n * AVOID:\n * - Treating the file as shared mutable state between processes. Atomic writes\n * prevent torn reads, not lost updates\n * - Reading the environment directly for anything this exposes\n */\n\n/** Points at the state file. The one variable that is not derived. */\nexport const HARNESS_STATE_POINTER = 'PO_STATE';\n\nconst STATE_FILE_VERSION = 1;\nconst STATE_FILE_NAME = 'harness-state.json';\nconst ADOPTED_DIRECTORY = 'doom-harness';\n/**\n * Written for a process that does not exist yet, so the first one to read it\n * owns it. A spawner cannot know its child's pid, and a child that inherited\n * its parent's file would lose it the moment the parent cleaned up.\n */\nconst UNCLAIMED_OWNER = 0;\n\nexport interface HarnessStateFile {\n version: number;\n owner: { pid: number; startedAt: string };\n state: HarnessState;\n}\n\nexport interface HarnessStateTransactionSnapshot {\n readonly filePath?: string;\n readonly owned: boolean;\n readonly state: HarnessState;\n readonly environment: Readonly<Record<string, string | undefined>>;\n}\n\n/** Drops the cached state. For tests, and for a process changing identity. */\nexport function resetHarnessStore(): void {\n harnessCache.loaded = undefined;\n}\n\nfunction fileMtime(filePath: string): number | undefined {\n try {\n return fs.statSync(filePath).mtimeMs;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Whether the cache still describes what the process points at.\n *\n * A session can load more than one copy of this package (a layer package\n * installed under .pi/npm brings its own), and each copy caches on its own.\n * The pointer and the file are shared, so a switch persisted through one copy\n * moves the pointer or rewrites the file, and every other copy must notice\n * rather than answer from memory.\n */\nfunction cacheCurrent(cache: CachedHarnessState, environment: NodeJS.ProcessEnv): boolean {\n const pointer = environment[HARNESS_STATE_POINTER];\n if (cache.filePath !== pointer) return false;\n if (!cache.filePath) return true;\n try {\n return fs.statSync(cache.filePath).mtimeMs === cache.mtimeMs;\n } catch {\n return false;\n }\n}\n\nfunction parseStateFile(source: string, filePath: string): HarnessStateFile {\n const parsed = JSON.parse(source) as Partial<HarnessStateFile>;\n if (parsed.version !== STATE_FILE_VERSION) {\n throw new Error(\n `Harness state at ${filePath} has version ${String(parsed.version)}, expected ${STATE_FILE_VERSION}`,\n );\n }\n if (typeof parsed.state !== 'object' || parsed.state === null) {\n throw new Error(`Harness state at ${filePath} has no state object`);\n }\n return {\n version: STATE_FILE_VERSION,\n owner: { pid: parsed.owner?.pid ?? -1, startedAt: parsed.owner?.startedAt ?? '' },\n state: parsed.state as HarnessState,\n };\n}\n\n/**\n * Reads the state for this process, once.\n *\n * A missing pointer means nobody wrote a file for this process, so the\n * environment is all there is: a nested run, a third-party spawn, or a caller\n * older than the store. A pointer that cannot be read is reported and then\n * treated the same way, because a session that starts degraded beats a session\n * that does not start at all.\n */\nexport function loadHarnessState(\n environment: NodeJS.ProcessEnv = process.env,\n report?: HarnessStateParseReporter,\n): LoadedHarnessState {\n // The cache describes this process. Asking about another environment is a\n // question about another process, so it reads through rather than answering\n // from what this one happens to hold.\n const cacheable = environment === process.env;\n if (cacheable && harnessCache.loaded && cacheCurrent(harnessCache.loaded, environment)) return harnessCache.loaded;\n\n const filePath = environment[HARNESS_STATE_POINTER];\n let result: CachedHarnessState | undefined;\n if (filePath && fs.existsSync(filePath)) {\n try {\n const file = parseStateFile(fs.readFileSync(filePath, 'utf8'), filePath);\n const owned = file.owner.pid === process.pid || file.owner.pid === UNCLAIMED_OWNER;\n result = { filePath, owned, state: file.state, mtimeMs: fileMtime(filePath) };\n } catch (error) {\n report?.(HARNESS_STATE_POINTER, error);\n }\n }\n result ??= { owned: false, state: readHarnessState(environment, report) };\n if (cacheable) harnessCache.loaded = result;\n return result;\n}\n\n/** Captures the owned state pointer and every projected harness variable. */\nexport function snapshotHarnessState(environment: NodeJS.ProcessEnv = process.env): HarnessStateTransactionSnapshot {\n const loadedState = loadHarnessState(environment);\n const environmentSnapshot: Record<string, string | undefined> = {\n [HARNESS_STATE_POINTER]: environment[HARNESS_STATE_POINTER],\n };\n for (const key of Object.values(HARNESS_STATE_KEYS)) environmentSnapshot[key] = environment[key];\n return {\n filePath: loadedState.filePath,\n owned: loadedState.owned,\n state: structuredClone(loadedState.state),\n environment: environmentSnapshot,\n };\n}\n\n/** Restores a snapshot only while the current process still owns its state pointer. */\nexport function restoreHarnessStateSnapshot(\n snapshot: HarnessStateTransactionSnapshot,\n environment: NodeJS.ProcessEnv = process.env,\n): void {\n const current = loadHarnessState(environment);\n if (snapshot.filePath && snapshot.owned) {\n if (!current.owned || current.filePath !== snapshot.filePath) {\n throw new Error('Harness state ownership changed; refusing to restore an older snapshot.');\n }\n persist(snapshot.filePath, snapshot.state);\n } else if (!snapshot.filePath) {\n if (current.filePath && !current.owned) {\n throw new Error('Harness state is no longer owned by this process; refusing to restore an older snapshot.');\n }\n if (current.filePath && current.owned) fs.rmSync(current.filePath, { force: true });\n resetHarnessStore();\n } else if (current.filePath !== snapshot.filePath) {\n throw new Error('Harness state pointer changed; refusing to restore an older snapshot.');\n }\n for (const [key, value] of Object.entries(snapshot.environment)) {\n if (value === undefined) delete environment[key];\n else environment[key] = value;\n }\n if (environment === process.env) {\n harnessCache.loaded = snapshot.filePath\n ? {\n filePath: snapshot.filePath,\n owned: snapshot.owned,\n state: snapshot.state,\n mtimeMs: fileMtime(snapshot.filePath),\n }\n : undefined;\n }\n}\n\nexport function getHarnessState(report?: HarnessStateParseReporter): HarnessState {\n return loadHarnessState(process.env, report).state;\n}\n\n/**\n * The harness state of one session, by id.\n *\n * A server runs many sessions' extensions in one process, each with its own state file, so\n * a session-scoped reader asks by session id. A terminal session owns its process, and the\n * answer is the same as `getHarnessState`.\n */\nexport function getSessionHarnessState(\n sessionId: string | undefined,\n report?: HarnessStateParseReporter,\n): HarnessState {\n return loadHarnessState(readDoomSessionEnvironment(sessionId), report).state;\n}\n\nfunction stateFilePath(directory: string): string {\n return path.join(directory, STATE_FILE_NAME);\n}\n\n/** Where a process writes state when it was not given a file of its own. */\nfunction adoptedStateFilePath(): string {\n return path.join(os.tmpdir(), ADOPTED_DIRECTORY, `${String(process.pid)}.json`);\n}\n\nfunction persist(filePath: string, state: HarnessState, ownerPid = process.pid): void {\n writePrivateAtomicJson(filePath, {\n version: STATE_FILE_VERSION,\n owner: { pid: ownerPid, startedAt: new Date().toISOString() },\n state,\n } satisfies HarnessStateFile);\n}\n\n/**\n * Starts a session: writes the file and publishes the derived environment.\n *\n * The environment is a parameter because the caller is usually describing a\n * process other than itself. The launcher builds the environment for the Pi it\n * is about to spawn, and Doom Team builds the snapshot for a detached child, so\n * neither should touch its own `process.env` to do it.\n */\nexport function createHarnessSession(\n state: HarnessState,\n options: {\n directory: string;\n environment: NodeJS.ProcessEnv;\n /**\n * Leave the file for a process that does not exist yet to claim.\n *\n * What a spawner wants: the child owns its state from its first read, in a\n * directory the spawner manages, so the parent cleaning up its own session\n * cannot pull the file out from under a detached run.\n */\n unclaimed?: boolean;\n },\n): string {\n const filePath = stateFilePath(options.directory);\n persist(filePath, state, options.unclaimed ? UNCLAIMED_OWNER : process.pid);\n options.environment[HARNESS_STATE_POINTER] = filePath;\n projectHarnessEnvironment(state, options.environment);\n // Only prime the cache when this process is the one being described.\n if (!options.unclaimed && options.environment === process.env)\n harnessCache.loaded = { filePath, owned: true, state, mtimeMs: fileMtime(filePath) };\n return filePath;\n}\n\n/**\n * Applies a patch to this process's state.\n *\n * Copy on write: a process holding a file it does not own writes its own copy\n * and repoints itself at it, so a child can never rewrite its parent's session.\n */\nexport function updateHarnessState(\n patch: Partial<HarnessState>,\n environment: NodeJS.ProcessEnv = process.env,\n): HarnessState {\n const current = loadHarnessState(environment);\n const state = { ...current.state, ...patch };\n const filePath = current.owned && current.filePath ? current.filePath : adoptedStateFilePath();\n persist(filePath, state);\n environment[HARNESS_STATE_POINTER] = filePath;\n if (environment === process.env) harnessCache.loaded = { filePath, owned: true, state, mtimeMs: fileMtime(filePath) };\n projectHarnessEnvironment(patch, environment);\n return state;\n}\n\n/** Removes this process's own state file. Safe when there is none. */\nexport function disposeHarnessState(environment: NodeJS.ProcessEnv = process.env): void {\n const current = harnessCache.loaded;\n resetHarnessStore();\n if (!current?.owned || !current.filePath) return;\n fs.rmSync(current.filePath, { force: true });\n delete environment[HARNESS_STATE_POINTER];\n}\n\nexport function harnessRoot(state: Pick<HarnessState, 'root'> = getHarnessState()): string {\n return state.root ?? process.cwd();\n}\n\n/**\n * The repository root, or a hard error.\n *\n * Separate from `requireHarnessPaths` because most callers only need the root,\n * and separate from `harnessRoot` because falling back to `process.cwd()` would\n * quietly operate on whatever directory the session happens to sit in. A caller\n * that asks for the root wants the session's repository or nothing.\n */\nexport function requireHarnessRoot(state: Pick<HarnessState, 'root'> = getHarnessState()): string {\n if (!state.root) throw new Error(`${HARNESS_STATE_KEYS.root} is not set`);\n return state.root;\n}\n\n/** The root and the run's scratch directory, for callers that write to disk. */\nexport function requireHarnessPaths(state: Pick<HarnessState, 'root' | 'temporaryDirectory'> = getHarnessState()): {\n root: string;\n temporaryDirectory: string;\n} {\n const root = requireHarnessRoot(state);\n if (!state.temporaryDirectory) throw new Error(`${HARNESS_STATE_KEYS.temporaryDirectory} is not set`);\n return { root, temporaryDirectory: state.temporaryDirectory };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { t as writePrivateAtomicJson } from "./atomicJson-1W0WhuFF.mjs";
|
|
2
|
+
import { i as readHarnessState, n as HARNESS_STATE_KEYS, r as projectHarnessEnvironment } from "./harnessState-Dn_S7fB9.mjs";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import { readDoomSessionEnvironment } from "@agimon-ai/po-core/cordisHost";
|
|
7
|
+
//#region src/models/harnessCache.ts
|
|
8
|
+
const harnessCache = { loaded: void 0 };
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/services/harnessStore/index.ts
|
|
11
|
+
/**
|
|
12
|
+
* Who owns the harness state, and where it lives.
|
|
13
|
+
*
|
|
14
|
+
* The state used to be the environment: twenty variables that every reader
|
|
15
|
+
* re-parsed and every writer mutated globally. The file is the authority now,
|
|
16
|
+
* and the environment is a projection published for the readers that can only
|
|
17
|
+
* see an environment, which are bash hooks, the shell launchers, packages that
|
|
18
|
+
* do not depend on this one, and any process spawned by any of them.
|
|
19
|
+
*
|
|
20
|
+
* OWNERSHIP IS PER PROCESS, AND THAT IS THE WHOLE DESIGN:
|
|
21
|
+
* Doom Team subagents run detached, in their own process group, from an
|
|
22
|
+
* environment snapshot taken at spawn. A child can outlive its parent. If it
|
|
23
|
+
* shared the parent's file it would corrupt a session it does not own, and if
|
|
24
|
+
* that file sat in the parent's run directory the parent's shutdown sweep would
|
|
25
|
+
* delete it from under a live run. So every process writes a file it owns:
|
|
26
|
+
* `owner.pid` records who, and a process that finds someone else's file copies
|
|
27
|
+
* it before its first write.
|
|
28
|
+
*
|
|
29
|
+
* AVOID:
|
|
30
|
+
* - Treating the file as shared mutable state between processes. Atomic writes
|
|
31
|
+
* prevent torn reads, not lost updates
|
|
32
|
+
* - Reading the environment directly for anything this exposes
|
|
33
|
+
*/
|
|
34
|
+
/** Points at the state file. The one variable that is not derived. */
|
|
35
|
+
const HARNESS_STATE_POINTER = "PO_STATE";
|
|
36
|
+
const STATE_FILE_VERSION = 1;
|
|
37
|
+
const STATE_FILE_NAME = "harness-state.json";
|
|
38
|
+
const ADOPTED_DIRECTORY = "doom-harness";
|
|
39
|
+
/**
|
|
40
|
+
* Written for a process that does not exist yet, so the first one to read it
|
|
41
|
+
* owns it. A spawner cannot know its child's pid, and a child that inherited
|
|
42
|
+
* its parent's file would lose it the moment the parent cleaned up.
|
|
43
|
+
*/
|
|
44
|
+
const UNCLAIMED_OWNER = 0;
|
|
45
|
+
/** Drops the cached state. For tests, and for a process changing identity. */
|
|
46
|
+
function resetHarnessStore() {
|
|
47
|
+
harnessCache.loaded = void 0;
|
|
48
|
+
}
|
|
49
|
+
function fileMtime(filePath) {
|
|
50
|
+
try {
|
|
51
|
+
return fs.statSync(filePath).mtimeMs;
|
|
52
|
+
} catch {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Whether the cache still describes what the process points at.
|
|
58
|
+
*
|
|
59
|
+
* A session can load more than one copy of this package (a layer package
|
|
60
|
+
* installed under .pi/npm brings its own), and each copy caches on its own.
|
|
61
|
+
* The pointer and the file are shared, so a switch persisted through one copy
|
|
62
|
+
* moves the pointer or rewrites the file, and every other copy must notice
|
|
63
|
+
* rather than answer from memory.
|
|
64
|
+
*/
|
|
65
|
+
function cacheCurrent(cache, environment) {
|
|
66
|
+
const pointer = environment[HARNESS_STATE_POINTER];
|
|
67
|
+
if (cache.filePath !== pointer) return false;
|
|
68
|
+
if (!cache.filePath) return true;
|
|
69
|
+
try {
|
|
70
|
+
return fs.statSync(cache.filePath).mtimeMs === cache.mtimeMs;
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function parseStateFile(source, filePath) {
|
|
76
|
+
const parsed = JSON.parse(source);
|
|
77
|
+
if (parsed.version !== STATE_FILE_VERSION) throw new Error(`Harness state at ${filePath} has version ${String(parsed.version)}, expected ${STATE_FILE_VERSION}`);
|
|
78
|
+
if (typeof parsed.state !== "object" || parsed.state === null) throw new Error(`Harness state at ${filePath} has no state object`);
|
|
79
|
+
return {
|
|
80
|
+
version: STATE_FILE_VERSION,
|
|
81
|
+
owner: {
|
|
82
|
+
pid: parsed.owner?.pid ?? -1,
|
|
83
|
+
startedAt: parsed.owner?.startedAt ?? ""
|
|
84
|
+
},
|
|
85
|
+
state: parsed.state
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Reads the state for this process, once.
|
|
90
|
+
*
|
|
91
|
+
* A missing pointer means nobody wrote a file for this process, so the
|
|
92
|
+
* environment is all there is: a nested run, a third-party spawn, or a caller
|
|
93
|
+
* older than the store. A pointer that cannot be read is reported and then
|
|
94
|
+
* treated the same way, because a session that starts degraded beats a session
|
|
95
|
+
* that does not start at all.
|
|
96
|
+
*/
|
|
97
|
+
function loadHarnessState(environment = process.env, report) {
|
|
98
|
+
const cacheable = environment === process.env;
|
|
99
|
+
if (cacheable && harnessCache.loaded && cacheCurrent(harnessCache.loaded, environment)) return harnessCache.loaded;
|
|
100
|
+
const filePath = environment[HARNESS_STATE_POINTER];
|
|
101
|
+
let result;
|
|
102
|
+
if (filePath && fs.existsSync(filePath)) try {
|
|
103
|
+
const file = parseStateFile(fs.readFileSync(filePath, "utf8"), filePath);
|
|
104
|
+
result = {
|
|
105
|
+
filePath,
|
|
106
|
+
owned: file.owner.pid === process.pid || file.owner.pid === UNCLAIMED_OWNER,
|
|
107
|
+
state: file.state,
|
|
108
|
+
mtimeMs: fileMtime(filePath)
|
|
109
|
+
};
|
|
110
|
+
} catch (error) {
|
|
111
|
+
report?.(HARNESS_STATE_POINTER, error);
|
|
112
|
+
}
|
|
113
|
+
result ??= {
|
|
114
|
+
owned: false,
|
|
115
|
+
state: readHarnessState(environment, report)
|
|
116
|
+
};
|
|
117
|
+
if (cacheable) harnessCache.loaded = result;
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
/** Captures the owned state pointer and every projected harness variable. */
|
|
121
|
+
function snapshotHarnessState(environment = process.env) {
|
|
122
|
+
const loadedState = loadHarnessState(environment);
|
|
123
|
+
const environmentSnapshot = { [HARNESS_STATE_POINTER]: environment[HARNESS_STATE_POINTER] };
|
|
124
|
+
for (const key of Object.values(HARNESS_STATE_KEYS)) environmentSnapshot[key] = environment[key];
|
|
125
|
+
return {
|
|
126
|
+
filePath: loadedState.filePath,
|
|
127
|
+
owned: loadedState.owned,
|
|
128
|
+
state: structuredClone(loadedState.state),
|
|
129
|
+
environment: environmentSnapshot
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/** Restores a snapshot only while the current process still owns its state pointer. */
|
|
133
|
+
function restoreHarnessStateSnapshot(snapshot, environment = process.env) {
|
|
134
|
+
const current = loadHarnessState(environment);
|
|
135
|
+
if (snapshot.filePath && snapshot.owned) {
|
|
136
|
+
if (!current.owned || current.filePath !== snapshot.filePath) throw new Error("Harness state ownership changed; refusing to restore an older snapshot.");
|
|
137
|
+
persist(snapshot.filePath, snapshot.state);
|
|
138
|
+
} else if (!snapshot.filePath) {
|
|
139
|
+
if (current.filePath && !current.owned) throw new Error("Harness state is no longer owned by this process; refusing to restore an older snapshot.");
|
|
140
|
+
if (current.filePath && current.owned) fs.rmSync(current.filePath, { force: true });
|
|
141
|
+
resetHarnessStore();
|
|
142
|
+
} else if (current.filePath !== snapshot.filePath) throw new Error("Harness state pointer changed; refusing to restore an older snapshot.");
|
|
143
|
+
for (const [key, value] of Object.entries(snapshot.environment)) if (value === void 0) delete environment[key];
|
|
144
|
+
else environment[key] = value;
|
|
145
|
+
if (environment === process.env) harnessCache.loaded = snapshot.filePath ? {
|
|
146
|
+
filePath: snapshot.filePath,
|
|
147
|
+
owned: snapshot.owned,
|
|
148
|
+
state: snapshot.state,
|
|
149
|
+
mtimeMs: fileMtime(snapshot.filePath)
|
|
150
|
+
} : void 0;
|
|
151
|
+
}
|
|
152
|
+
function getHarnessState(report) {
|
|
153
|
+
return loadHarnessState(process.env, report).state;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The harness state of one session, by id.
|
|
157
|
+
*
|
|
158
|
+
* A server runs many sessions' extensions in one process, each with its own state file, so
|
|
159
|
+
* a session-scoped reader asks by session id. A terminal session owns its process, and the
|
|
160
|
+
* answer is the same as `getHarnessState`.
|
|
161
|
+
*/
|
|
162
|
+
function getSessionHarnessState(sessionId, report) {
|
|
163
|
+
return loadHarnessState(readDoomSessionEnvironment(sessionId), report).state;
|
|
164
|
+
}
|
|
165
|
+
function stateFilePath(directory) {
|
|
166
|
+
return path.join(directory, STATE_FILE_NAME);
|
|
167
|
+
}
|
|
168
|
+
/** Where a process writes state when it was not given a file of its own. */
|
|
169
|
+
function adoptedStateFilePath() {
|
|
170
|
+
return path.join(os.tmpdir(), ADOPTED_DIRECTORY, `${String(process.pid)}.json`);
|
|
171
|
+
}
|
|
172
|
+
function persist(filePath, state, ownerPid = process.pid) {
|
|
173
|
+
writePrivateAtomicJson(filePath, {
|
|
174
|
+
version: STATE_FILE_VERSION,
|
|
175
|
+
owner: {
|
|
176
|
+
pid: ownerPid,
|
|
177
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
178
|
+
},
|
|
179
|
+
state
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Starts a session: writes the file and publishes the derived environment.
|
|
184
|
+
*
|
|
185
|
+
* The environment is a parameter because the caller is usually describing a
|
|
186
|
+
* process other than itself. The launcher builds the environment for the Pi it
|
|
187
|
+
* is about to spawn, and Doom Team builds the snapshot for a detached child, so
|
|
188
|
+
* neither should touch its own `process.env` to do it.
|
|
189
|
+
*/
|
|
190
|
+
function createHarnessSession(state, options) {
|
|
191
|
+
const filePath = stateFilePath(options.directory);
|
|
192
|
+
persist(filePath, state, options.unclaimed ? UNCLAIMED_OWNER : process.pid);
|
|
193
|
+
options.environment[HARNESS_STATE_POINTER] = filePath;
|
|
194
|
+
projectHarnessEnvironment(state, options.environment);
|
|
195
|
+
if (!options.unclaimed && options.environment === process.env) harnessCache.loaded = {
|
|
196
|
+
filePath,
|
|
197
|
+
owned: true,
|
|
198
|
+
state,
|
|
199
|
+
mtimeMs: fileMtime(filePath)
|
|
200
|
+
};
|
|
201
|
+
return filePath;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Applies a patch to this process's state.
|
|
205
|
+
*
|
|
206
|
+
* Copy on write: a process holding a file it does not own writes its own copy
|
|
207
|
+
* and repoints itself at it, so a child can never rewrite its parent's session.
|
|
208
|
+
*/
|
|
209
|
+
function updateHarnessState(patch, environment = process.env) {
|
|
210
|
+
const current = loadHarnessState(environment);
|
|
211
|
+
const state = {
|
|
212
|
+
...current.state,
|
|
213
|
+
...patch
|
|
214
|
+
};
|
|
215
|
+
const filePath = current.owned && current.filePath ? current.filePath : adoptedStateFilePath();
|
|
216
|
+
persist(filePath, state);
|
|
217
|
+
environment[HARNESS_STATE_POINTER] = filePath;
|
|
218
|
+
if (environment === process.env) harnessCache.loaded = {
|
|
219
|
+
filePath,
|
|
220
|
+
owned: true,
|
|
221
|
+
state,
|
|
222
|
+
mtimeMs: fileMtime(filePath)
|
|
223
|
+
};
|
|
224
|
+
projectHarnessEnvironment(patch, environment);
|
|
225
|
+
return state;
|
|
226
|
+
}
|
|
227
|
+
/** Removes this process's own state file. Safe when there is none. */
|
|
228
|
+
function disposeHarnessState(environment = process.env) {
|
|
229
|
+
const current = harnessCache.loaded;
|
|
230
|
+
resetHarnessStore();
|
|
231
|
+
if (!current?.owned || !current.filePath) return;
|
|
232
|
+
fs.rmSync(current.filePath, { force: true });
|
|
233
|
+
delete environment[HARNESS_STATE_POINTER];
|
|
234
|
+
}
|
|
235
|
+
function harnessRoot(state = getHarnessState()) {
|
|
236
|
+
return state.root ?? process.cwd();
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* The repository root, or a hard error.
|
|
240
|
+
*
|
|
241
|
+
* Separate from `requireHarnessPaths` because most callers only need the root,
|
|
242
|
+
* and separate from `harnessRoot` because falling back to `process.cwd()` would
|
|
243
|
+
* quietly operate on whatever directory the session happens to sit in. A caller
|
|
244
|
+
* that asks for the root wants the session's repository or nothing.
|
|
245
|
+
*/
|
|
246
|
+
function requireHarnessRoot(state = getHarnessState()) {
|
|
247
|
+
if (!state.root) throw new Error(`${HARNESS_STATE_KEYS.root} is not set`);
|
|
248
|
+
return state.root;
|
|
249
|
+
}
|
|
250
|
+
/** The root and the run's scratch directory, for callers that write to disk. */
|
|
251
|
+
function requireHarnessPaths(state = getHarnessState()) {
|
|
252
|
+
const root = requireHarnessRoot(state);
|
|
253
|
+
if (!state.temporaryDirectory) throw new Error(`${HARNESS_STATE_KEYS.temporaryDirectory} is not set`);
|
|
254
|
+
return {
|
|
255
|
+
root,
|
|
256
|
+
temporaryDirectory: state.temporaryDirectory
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
//#endregion
|
|
260
|
+
export { getSessionHarnessState as a, requireHarnessPaths as c, restoreHarnessStateSnapshot as d, snapshotHarnessState as f, getHarnessState as i, requireHarnessRoot as l, createHarnessSession as n, harnessRoot as o, updateHarnessState as p, disposeHarnessState as r, loadHarnessState as s, HARNESS_STATE_POINTER as t, resetHarnessStore as u };
|
|
261
|
+
|
|
262
|
+
//# sourceMappingURL=harnessStore-CltwLB7M.mjs.map
|