@proagentstore/cli 0.4.92 → 0.4.94
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.
|
@@ -13,16 +13,35 @@ export class CodingRuntime {
|
|
|
13
13
|
reposBaseDir;
|
|
14
14
|
sessions = new Map();
|
|
15
15
|
/**
|
|
16
|
-
* Coding
|
|
16
|
+
* Coding turns that are running here (#896, #1007).
|
|
17
17
|
*
|
|
18
|
-
* A
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* A retained session is a conversation, not necessarily work in flight: a healthy engine remains
|
|
19
|
+
* available at `runState: "idle"` between turns. Runner updates must protect only turns that can
|
|
20
|
+
* be interrupted. If this runtime cannot observe a session's state, retain the conservative busy
|
|
21
|
+
* answer rather than restarting across work whose state it cannot prove is idle.
|
|
21
22
|
*/
|
|
22
23
|
liveWork() {
|
|
23
|
-
return [...this.sessions.values()].
|
|
24
|
+
return [...this.sessions.values()].flatMap((s) => {
|
|
24
25
|
const engine = s.clientType ?? "";
|
|
25
|
-
|
|
26
|
+
const label = engine === "claude" ? "a Claude coding session (resumes after a restart)" : `a ${engine || "coding"} turn (lost on a restart)`;
|
|
27
|
+
try {
|
|
28
|
+
// `alive: false` is an observed inactive session. Any other answer (including a
|
|
29
|
+
// malformed older implementation) is unknown and must fail closed below.
|
|
30
|
+
const alive = s.alive;
|
|
31
|
+
if (alive === false)
|
|
32
|
+
return [];
|
|
33
|
+
const state = s.runState();
|
|
34
|
+
if (alive === true && state === "idle")
|
|
35
|
+
return [];
|
|
36
|
+
if (alive === true && (state === "thinking" || state === "responding"))
|
|
37
|
+
return [label];
|
|
38
|
+
// An unrecognised `alive`/`runState` pair is not proof that an engine is idle.
|
|
39
|
+
return [label];
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Do not turn a failed state probe into permission to restart the machine.
|
|
43
|
+
return [label];
|
|
44
|
+
}
|
|
26
45
|
});
|
|
27
46
|
}
|
|
28
47
|
/** Background cold-start clones, one per folder (#858). */
|
|
@@ -20,6 +20,8 @@ export const LOCAL_ARTIFACT_TASK_TYPE = "local_artifact.generate";
|
|
|
20
20
|
export const LOCAL_ARTIFACT_RUN_PATH = "/local-artifact/run";
|
|
21
21
|
export const LOCAL_ARTIFACT_STATUS_PATH = "/local-artifact/status";
|
|
22
22
|
export const LOCAL_ARTIFACT_CANCEL_PATH = "/local-artifact/cancel";
|
|
23
|
+
/** First CLI that consumes an explicit uploaded source instead of its local master files. */
|
|
24
|
+
export const LOCAL_ARTIFACT_UPLOADED_SOURCES_MIN_CLI = "0.4.93";
|
|
23
25
|
/** The readiness event a completed tailoring run emits, once, through the connection outbox. */
|
|
24
26
|
export const MATERIALS_READY_EVENT = "job.application.materials_ready";
|
|
25
27
|
export const LOCAL_ARTIFACT_ENGINES = ["claude", "codex"];
|
|
@@ -67,6 +67,26 @@ export function parseArtifactEnvelope(raw) {
|
|
|
67
67
|
throw bad(`source ${String(r.role)} is listed twice`);
|
|
68
68
|
sources.push({ role: r.role, path: r.path });
|
|
69
69
|
}
|
|
70
|
+
const uploadedSources = [];
|
|
71
|
+
if (o.uploadedSources !== undefined) {
|
|
72
|
+
if (!Array.isArray(o.uploadedSources) || o.uploadedSources.length !== 2)
|
|
73
|
+
throw bad("uploadedSources must contain the selected resume and profile");
|
|
74
|
+
for (const source of o.uploadedSources) {
|
|
75
|
+
const r = (source && typeof source === "object" ? source : {});
|
|
76
|
+
if ((r.role !== "resume" && r.role !== "profile") || uploadedSources.some((x) => x.role === r.role))
|
|
77
|
+
throw bad("uploaded sources need one resume and one profile");
|
|
78
|
+
if (typeof r.fileId !== "string" || !PATH_SEGMENT.test(r.fileId) || typeof r.version !== "string" || !r.version || r.version.length > 300 || typeof r.originalSha256 !== "string" || !/^[a-f0-9]{64}$/.test(r.originalSha256) || typeof r.extractedTextSha256 !== "string" || !/^[a-f0-9]{64}$/.test(r.extractedTextSha256) || typeof r.extractedAt !== "string" || Number.isNaN(Date.parse(r.extractedAt)) || typeof r.text !== "string")
|
|
79
|
+
throw bad("uploaded source provenance is invalid");
|
|
80
|
+
const bytes = Buffer.byteLength(r.text, "utf8");
|
|
81
|
+
if (!bytes || bytes > LOCAL_ARTIFACT_CAPS.sourceBytes)
|
|
82
|
+
throw bad(`uploaded source must be 1-${LOCAL_ARTIFACT_CAPS.sourceBytes} UTF-8 bytes`);
|
|
83
|
+
if (sha256(r.text) !== r.extractedTextSha256)
|
|
84
|
+
throw bad("uploaded source text does not match its extracted-text hash");
|
|
85
|
+
uploadedSources.push({ role: r.role, fileId: r.fileId, version: r.version, originalSha256: r.originalSha256, extractedTextSha256: r.extractedTextSha256, extractedAt: r.extractedAt, text: r.text });
|
|
86
|
+
}
|
|
87
|
+
if (uploadedSources.length !== 2 || !uploadedSources.some((s) => s.role === "resume") || !uploadedSources.some((s) => s.role === "profile"))
|
|
88
|
+
throw bad("uploaded sources need one resume and one profile");
|
|
89
|
+
}
|
|
70
90
|
const p = (o.policy && typeof o.policy === "object" ? o.policy : {});
|
|
71
91
|
const retainDays = intIn(p.retainDays, 0, 3650);
|
|
72
92
|
const maxMinutes = intIn(p.maxMinutes, 1, 60);
|
|
@@ -83,6 +103,7 @@ export function parseArtifactEnvelope(raw) {
|
|
|
83
103
|
authMode: o.authMode,
|
|
84
104
|
workspace: o.workspace,
|
|
85
105
|
sources,
|
|
106
|
+
...(uploadedSources.length ? { uploadedSources } : {}),
|
|
86
107
|
policy: { retainDays, maxMinutes, maxConcurrent },
|
|
87
108
|
...("lead" in lead ? { lead: lead.lead } : { lead: null, leadError: lead.error }),
|
|
88
109
|
};
|
|
@@ -163,31 +184,53 @@ export class LocalArtifactRuntime {
|
|
|
163
184
|
throw new Pause("workspace_unavailable", [`The workspace ${e.workspace} cannot be used: ${err instanceof Error ? err.message : String(err)}`]);
|
|
164
185
|
}
|
|
165
186
|
this.sweepArtifacts(run, workspace);
|
|
166
|
-
// Sources:
|
|
187
|
+
// Sources: uploaded selections arrive only over the authenticated relay, are written beneath
|
|
188
|
+
// this run's private scratch folder under fixed role names, and never use a user filename/path.
|
|
189
|
+
// Local mode remains the existing path. Content never reaches events/results/DB.
|
|
167
190
|
const texts = [];
|
|
168
191
|
const hashes = [];
|
|
169
192
|
const missing = [];
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
193
|
+
if (e.uploadedSources?.length) {
|
|
194
|
+
const sourceDir = join(run.scratch, "sources");
|
|
195
|
+
mkdirSync(sourceDir, { recursive: true, mode: 0o700 });
|
|
196
|
+
for (const s of e.uploadedSources) {
|
|
197
|
+
const safePath = join(sourceDir, `${s.role}.txt`);
|
|
198
|
+
try {
|
|
199
|
+
writeFileSync(safePath, s.text, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
200
|
+
const bytes = Buffer.byteLength(s.text, "utf8");
|
|
201
|
+
const hash = { role: s.role, path: `uploaded/${s.fileId}@${s.version}`, sha256: s.extractedTextSha256, bytes };
|
|
202
|
+
texts.push({ role: s.role, text: readFileSync(safePath, "utf8") });
|
|
203
|
+
hashes.push(hash);
|
|
204
|
+
this.emit(run, { type: "source.read", detail: hash });
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
this.emit(run, { type: "source.missing", detail: { role: s.role, path: `uploaded/${s.fileId}` } });
|
|
208
|
+
missing.push(`The selected uploaded ${s.role} could not be materialized (${err instanceof Error ? err.message : "unknown error"}).`);
|
|
209
|
+
}
|
|
187
210
|
}
|
|
188
211
|
}
|
|
212
|
+
else
|
|
213
|
+
for (const s of e.sources) {
|
|
214
|
+
const shown = `${e.workspace}/${s.path}`;
|
|
215
|
+
try {
|
|
216
|
+
const file = resolveSource(workspace, s.path);
|
|
217
|
+
const buf = readFileSync(file);
|
|
218
|
+
if (buf.length > LOCAL_ARTIFACT_CAPS.sourceBytes)
|
|
219
|
+
throw new Error(`larger than ${LOCAL_ARTIFACT_CAPS.sourceBytes / 1024} KB`);
|
|
220
|
+
if (!buf.length)
|
|
221
|
+
throw new Error("empty");
|
|
222
|
+
texts.push({ role: s.role, text: buf.toString("utf8") });
|
|
223
|
+
const hash = { role: s.role, path: shown, sha256: sha256(buf), bytes: buf.length };
|
|
224
|
+
hashes.push(hash);
|
|
225
|
+
this.emit(run, { type: "source.read", detail: hash });
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
this.emit(run, { type: "source.missing", detail: { role: s.role, path: shown } });
|
|
229
|
+
missing.push(`Your ${s.role} at ${shown} could not be read (${err instanceof Error && !/ENOENT/.test(err.message) ? err.message : "not found"}). Add it, or choose another file in the Application Tailor settings.`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
189
232
|
for (const role of REQUIRED_SOURCE_ROLES) {
|
|
190
|
-
if (!e.sources.some((s) => s.role === role))
|
|
233
|
+
if (e.uploadedSources?.length ? !e.uploadedSources.some((s) => s.role === role) : !e.sources.some((s) => s.role === role))
|
|
191
234
|
missing.push(`No ${role} source is configured. Choose one in the Application Tailor settings.`);
|
|
192
235
|
}
|
|
193
236
|
if (missing.length)
|
package/dist/index.js
CHANGED
|
@@ -1970,9 +1970,11 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
|
|
|
1970
1970
|
const observationsUnavailable = sessionsResult.status !== "fulfilled" || healthResult.status !== "fulfilled" || !healthResult.value.work;
|
|
1971
1971
|
const sessions = sessionsResult.status === "fulfilled" ? sessionsResult.value : { sessions: [] };
|
|
1972
1972
|
const health = healthResult.status === "fulfilled" ? healthResult.value : {};
|
|
1973
|
-
const activeCoding = (sessions.sessions ?? []).filter((s) => s.alive && s.runState
|
|
1973
|
+
const activeCoding = (sessions.sessions ?? []).filter((s) => s.alive !== false && (s.alive !== true || !["idle", "thinking", "responding"].includes(s.runState ?? "") || s.runState !== "idle")).map((s) => s.sessionId || "coding-session-state-unavailable");
|
|
1974
1974
|
const localRuns = health.work?.localRuns ?? 0;
|
|
1975
1975
|
const codingTurns = health.work?.codingTurns ?? 0;
|
|
1976
|
+
const unmatchedCodingTurns = Math.max(0, codingTurns - activeCoding.length);
|
|
1977
|
+
const localDetail = health.work?.detail?.slice(codingTurns) ?? [];
|
|
1976
1978
|
return {
|
|
1977
1979
|
current: CLI_VERSION,
|
|
1978
1980
|
latest: await latestPublishedVersion(),
|
|
@@ -1980,7 +1982,7 @@ async function connectViaRelay(instanceIds, localUrl, runnerToken, opts, force =
|
|
|
1980
1982
|
restarter: restarterFrom(process.env),
|
|
1981
1983
|
// A restart ends browser/application work too. Give these durable names to the same
|
|
1982
1984
|
// planner so neither manual nor automatic update can mistake them for an idle machine.
|
|
1983
|
-
busy: observationsUnavailable ? ["runner-work-observation-unavailable"] : [...activeCoding, ...Array.from({ length:
|
|
1985
|
+
busy: observationsUnavailable ? ["runner-work-observation-unavailable"] : [...activeCoding, ...Array.from({ length: unmatchedCodingTurns }, (_v, i) => `coding-turn-unmatched-${i + 1}`), ...Array.from({ length: localRuns }, (_v, i) => localDetail[i] || `local-run-${i + 1}`)]
|
|
1984
1986
|
};
|
|
1985
1987
|
};
|
|
1986
1988
|
let updateInstallInFlight = false;
|