@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 sessions that are running here (#896).
16
+ * Coding turns that are running here (#896, #1007).
17
17
  *
18
- * A Claude session survives a restart — it is re-spawned with `--resume <session_id>` and only
19
- * the turn in flight is cut — but a Codex, Grok or raw-engine session restarts cold and its turn
20
- * is gone. Both are named, because the owner is the one deciding whether to take the machine.
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()].map((s) => {
24
+ return [...this.sessions.values()].flatMap((s) => {
24
25
  const engine = s.clientType ?? "";
25
- return engine === "claude" ? "a Claude coding session (resumes after a restart)" : `a ${engine || "coding"} turn (lost on a restart)`;
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: read by the runtime, hashed, handed to the CLI in its prompt. Content never leaves this function except into the prompt.
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
- for (const s of e.sources) {
171
- const shown = `${e.workspace}/${s.path}`;
172
- try {
173
- const file = resolveSource(workspace, s.path);
174
- const buf = readFileSync(file);
175
- if (buf.length > LOCAL_ARTIFACT_CAPS.sourceBytes)
176
- throw new Error(`larger than ${LOCAL_ARTIFACT_CAPS.sourceBytes / 1024} KB`);
177
- if (!buf.length)
178
- throw new Error("empty");
179
- texts.push({ role: s.role, text: buf.toString("utf8") });
180
- const hash = { role: s.role, path: shown, sha256: sha256(buf), bytes: buf.length };
181
- hashes.push(hash);
182
- this.emit(run, { type: "source.read", detail: hash });
183
- }
184
- catch (err) {
185
- this.emit(run, { type: "source.missing", detail: { role: s.role, path: shown } });
186
- 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.`);
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 && s.runState !== "idle").map((s) => s.sessionId);
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: codingTurns }, (_v, i) => `coding-turn-${i + 1}`), ...Array.from({ length: localRuns }, (_v, i) => health.work?.detail?.[i] || `local-run-${i + 1}`)]
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.4.92",
3
+ "version": "0.4.94",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",