@proagentstore/cli 0.4.105 → 0.4.106

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.
@@ -1,3 +1,4 @@
1
+ import { InputLedger } from "./input-ledger.js";
1
2
  import { runObjectiveBlock } from "./run-objective.js";
2
3
  import { spawn } from "node:child_process";
3
4
  import { classifyCommand, commandFromToolInput, fillTargetFromResult, toolCallOk, toolResultMark } from "./engine-acts.js";
@@ -22,8 +23,6 @@ export { defaultStatePath } from "./headless-state.js";
22
23
  * (refusing new records) would lose the turns that just happened rather than ancient ones.
23
24
  */
24
25
  const MAX_PENDING_USAGE = 200;
25
- /** How many consumed-input ids a session reports (#1019). Re-sent on every snapshot, so bounded; the oldest fall off. */
26
- export const MAX_CONSUMED_INPUTS = 50;
27
26
  /**
28
27
  * How many un-drained consequential acts a session holds (#294).
29
28
  *
@@ -102,10 +101,8 @@ export class HeadlessSession {
102
101
  pendingInputs = [];
103
102
  /** The id of the input the current one-shot turn was spawned for (#1019). */
104
103
  turnInputId = null;
105
- /** Ids of inputs a COMPLETED turn consumed, newest last and bounded (#1019). See {@link consumedInputs}. */
106
- consumed = [];
107
- /** Ids of inputs handed to the engine, newest last and bounded (#1019): what a retry is reconciled against. */
108
- accepted = [];
104
+ /** Inputs accepted and consumed, bound to their content and the engine process that read them (#1019). */
105
+ inputs = new InputLedger();
109
106
  /**
110
107
  * The engine's own last output line of the turn in flight (#545).
111
108
  *
@@ -321,18 +318,11 @@ export class HeadlessSession {
321
318
  * credit for something it had not read.
322
319
  */
323
320
  get consumedInputs() {
324
- return this.consumed;
321
+ return this.inputs.consumedIds;
325
322
  }
326
323
  /** Inputs this session accepted — handed to the engine, whether or not a turn has finished on them. */
327
324
  get acceptedInputs() {
328
- return this.accepted;
329
- }
330
- credit(id) {
331
- if (!id)
332
- return;
333
- this.consumed.push(id);
334
- if (this.consumed.length > MAX_CONSUMED_INPUTS)
335
- this.consumed.shift();
325
+ return this.inputs.acceptedIds;
336
326
  }
337
327
  /** The engine process's pid while one is running — what per-session resource attribution reads (#924). */
338
328
  get pid() {
@@ -433,6 +423,7 @@ export class HeadlessSession {
433
423
  return;
434
424
  // A new process has read none of what the old one was sent, and owns none of its half-read line.
435
425
  this.pendingInputs = [];
426
+ this.inputs.newEngine();
436
427
  this.buf = "";
437
428
  const args = this.adapter.buildLaunchArgs(this.cmdArgs, this.claudeSessionId);
438
429
  const proc = spawn(this.cmdBin, args, {
@@ -512,11 +503,6 @@ export class HeadlessSession {
512
503
  * session-start brief, being the same record composed later.
513
504
  */
514
505
  input(text, opts = {}) {
515
- if (opts.inputId) {
516
- this.accepted.push(opts.inputId);
517
- if (this.accepted.length > MAX_CONSUMED_INPUTS)
518
- this.accepted.shift();
519
- }
520
506
  // The Engine reads the preamble, the pane the short marker; the instruction stays evidence.
521
507
  const sent = authoredTurn(text, opts.author);
522
508
  // The brief leads the turn and is spent in the reading (#693). It goes to the ENGINE only:
@@ -529,6 +515,8 @@ export class HeadlessSession {
529
515
  this.pendingSeed = null;
530
516
  if (!this.alive)
531
517
  this.start();
518
+ if (opts.inputId)
519
+ this.inputs.accept(opts.inputId, opts.digest ?? ""); // after start(): accepted by the process that will read it
532
520
  if (replay)
533
521
  this.push("[pags] this engine keeps no memory between turns, so ProAgentStore's record of the conversation so far was delivered with this turn — a reconstruction, not the engine's own memory");
534
522
  else if (seed)
@@ -700,7 +688,7 @@ export class HeadlessSession {
700
688
  this.turnReport = turnReportFromExit(code, signal, this.turnLastLine, Date.now(), this.turnTail);
701
689
  // A turn we killed did not finish reading its input (#1019).
702
690
  if (this.turnReport.verdict !== "killed")
703
- this.credit(inputId);
691
+ this.inputs.consume(inputId);
704
692
  this.run = "idle";
705
693
  this.proc = null;
706
694
  });
@@ -875,7 +863,7 @@ export class HeadlessSession {
875
863
  // own `is_error` — the same claim, in the words the engine states it in. Without
876
864
  // this the field would exist for three engines and silently not for the flagship.
877
865
  this.turnReport = turnReportFromResult(ev.isError, failure, Date.now(), this.turnTail);
878
- this.credit(this.pendingInputs.shift() ?? null);
866
+ this.inputs.consume(this.pendingInputs.shift() ?? null);
879
867
  // The same event that ends the turn also reports what the turn COST (#267). It was
880
868
  // parsed and thrown away, which is why Engine spend was absent from the ledger.
881
869
  // An errored turn still burned tokens, so this is recorded regardless of is_error.
@@ -0,0 +1,65 @@
1
+ // What a coding session accepted and consumed, bound to CONTENT and to the engine PROCESS (#1019).
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ /** Bound on each list, as before: enough to reconcile the latest acts, never a growing history. */
4
+ export const MAX_LEDGER_INPUTS = 50;
5
+ /** How many of the newest entries ride every snapshot — a retry reconciles the act it just sent. */
6
+ export const REPORTED_LEDGER_INPUTS = 16;
7
+ const str = (v) => (typeof v === "string" ? v : null);
8
+ /**
9
+ * sha256 of an act's EXECUTABLE content — text, author and objective — as received, never its delivery
10
+ * metadata (id, replay). The same id with different content is a conflict, not a retry. Mirrored by the
11
+ * cloud's `actDigest` (workers/api/src/lib/coding-act-send.ts); the two must agree byte for byte.
12
+ */
13
+ export function actDigest(a) {
14
+ const o = a.objective;
15
+ const objective = o && typeof o === "object" ? [str(o.runId), str(o.text), str(o.origin?.kind), str(o.origin?.by), Array.isArray(o.constraints) ? o.constraints.filter((c) => typeof c === "string") : []] : null;
16
+ return createHash("sha256").update(JSON.stringify([str(a.text), str(a.author), objective])).digest("hex");
17
+ }
18
+ /**
19
+ * One session's input record. `ledger` names THIS record: an id absent from it proves only that this
20
+ * ledger never accepted it — a different runner, or this session re-created after a runner restart, starts
21
+ * empty and cannot speak for its predecessor. `epoch` names the engine process: a replacement process has
22
+ * read nothing its predecessor read, so consumption is credited to the process that did the reading.
23
+ * Ids are never forgotten across a process change, so a retried id is still refused rather than run twice.
24
+ */
25
+ export class InputLedger {
26
+ ledger = randomUUID();
27
+ epoch = 0;
28
+ accepted = [];
29
+ consumed = [];
30
+ /** `duplicate`: this id with this content was already accepted. `conflict`: this id, different content. */
31
+ check(id, digest) {
32
+ const hit = this.accepted.find((a) => a.id === id);
33
+ return !hit ? "new" : hit.digest === digest ? "duplicate" : "conflict";
34
+ }
35
+ accept(id, digest) {
36
+ this.accepted.push({ id, digest, epoch: this.epoch });
37
+ if (this.accepted.length > MAX_LEDGER_INPUTS)
38
+ this.accepted.shift();
39
+ }
40
+ consume(id) {
41
+ if (!id)
42
+ return;
43
+ this.consumed.push({ id, epoch: this.epoch });
44
+ if (this.consumed.length > MAX_LEDGER_INPUTS)
45
+ this.consumed.shift();
46
+ }
47
+ /** A new engine process replaced the old one. */
48
+ newEngine() {
49
+ this.epoch++;
50
+ }
51
+ get acceptedIds() {
52
+ return this.accepted.map((a) => a.id);
53
+ }
54
+ get consumedIds() {
55
+ return this.consumed.map((c) => c.id);
56
+ }
57
+ report() {
58
+ return {
59
+ ledger: this.ledger,
60
+ epoch: this.epoch,
61
+ accepted: this.accepted.slice(-REPORTED_LEDGER_INPUTS).map((a) => [a.id, a.digest, a.epoch]),
62
+ consumed: this.consumed.slice(-REPORTED_LEDGER_INPUTS).map((c) => [c.id, c.epoch]),
63
+ };
64
+ }
65
+ }
@@ -1,5 +1,8 @@
1
1
  // The run's objective, delivered verbatim with a turn and attributed to the platform (#1019).
2
2
  const ORIGIN_KINDS = new Set(["account", "supervisor", "trigger", "unverified"]);
3
+ /** Bounds on the constraint lines a run objective may carry: they are labels, never an essay. */
4
+ const MAX_CONSTRAINTS = 20;
5
+ const MAX_CONSTRAINT_CHARS = 500;
3
6
  /** A run objective as the cloud sent it, or undefined — narrowed, because any caller can POST here (#1019). */
4
7
  export function runObjective(raw) {
5
8
  const o = raw;
@@ -10,7 +13,9 @@ export function runObjective(raw) {
10
13
  // A label longer than a label is not shortened into a different claim — it is dropped.
11
14
  const label = typeof origin?.by === "string" ? origin.by.replace(/\s+/g, " ").trim() : "";
12
15
  const by = label.length <= 128 ? label : "";
13
- return { runId: o.runId, text: o.text, origin: by ? { kind, by } : { kind } };
16
+ // Over-long or excess lines are dropped, never shortened: a truncated constraint is a different one.
17
+ const lines = Array.isArray(o.constraints) ? o.constraints.filter((c) => typeof c === "string" && !!c.trim() && c.length <= MAX_CONSTRAINT_CHARS).slice(0, MAX_CONSTRAINTS) : [];
18
+ return { runId: o.runId, text: o.text, origin: by ? { kind, by } : { kind }, ...(lines.length ? { constraints: lines } : {}) };
14
19
  }
15
20
  /** The attribution sentence: only what the platform verified about the author, never more. */
16
21
  function attribution(runId, origin) {
@@ -28,14 +33,19 @@ function attribution(runId, origin) {
28
33
  }
29
34
  /**
30
35
  * The run's objective as the engine reads it (#1019): verbatim, fenced, attributed only as far as the
31
- * platform verified — the Pilot's instruction that follows is its own turn, and owner directives sent
32
- * later outrank both.
36
+ * platform verified — the Pilot's instruction that follows is its own turn. The owner's standing rules and
37
+ * directives, earlier or later, outrank both, and the constraints in force when this turn was sent are
38
+ * spelled out beneath the fence so the precedence is not only a sentence.
33
39
  */
34
40
  export function runObjectiveBlock(o) {
41
+ const constraints = o.constraints?.length
42
+ ? ["[pags] CONSTRAINTS IN FORCE NOW — from the owner's rules and directives of this run, however old. Where the objective above conflicts with one, the constraint wins:", ...o.constraints]
43
+ : [];
35
44
  return [
36
- `[pags] RUN OBJECTIVE — ${attribution(o.runId, o.origin ?? { kind: "unverified" })} It is not the Pilot's instruction, which follows as its own turn. Instructions the owner sends later in this run take precedence over it.`,
45
+ `[pags] RUN OBJECTIVE — ${attribution(o.runId, o.origin ?? { kind: "unverified" })} It is not the Pilot's instruction, which follows as its own turn. The owner's standing rules and directives — earlier in this run or later — take precedence over it.`,
37
46
  "<<<OBJECTIVE",
38
47
  o.text,
39
48
  "OBJECTIVE>>>",
49
+ ...constraints,
40
50
  ].join("\n");
41
51
  }
@@ -1,3 +1,4 @@
1
+ import { actDigest } from "./input-ledger.js";
1
2
  import { runObjective } from "./run-objective.js";
2
3
  import { homedir } from "node:os";
3
4
  import { join, resolve } from "node:path";
@@ -71,7 +72,9 @@ export class CodingRuntime {
71
72
  // sends the objective only to a v2 runner, so an older one is never made to call it the owner's.
72
73
  // `coding.input-idempotent`: a message whose inputId this session already accepted is not run again,
73
74
  // and every snapshot lists the accepted ids, so a retry after a lost reply can be reconciled.
74
- return ["coding.sessions", "coding.stream", "human.takeover", "coding.inspect", "coding.repo-write", "coding.run-objective", "coding.run-objective.v2", "coding.input-idempotent"];
75
+ // `.v3` renders the owner's constraints in force with the objective; `coding.engine-identity`: every snapshot
76
+ // names its input ledger and engine process, and an id is deduplicated together with its content (input-ledger.ts).
77
+ return ["coding.sessions", "coding.stream", "human.takeover", "coding.inspect", "coding.repo-write", "coding.run-objective", "coding.run-objective.v2", "coding.run-objective.v3", "coding.input-idempotent", "coding.engine-identity"];
75
78
  }
76
79
  /**
77
80
  * Resolve the workDir for a read-only inspection. Prefer the tracked session's real
@@ -248,6 +251,7 @@ export class CodingRuntime {
248
251
  ...(session.lastTurn ? { lastTurn: session.lastTurn } : {}),
249
252
  ...(session.consumedInputs.length ? { consumedInputs: [...session.consumedInputs] } : {}),
250
253
  acceptedInputs: [...session.acceptedInputs],
254
+ engine: session.inputs.report(),
251
255
  ...(opts.drainUsage ? { usage: session.takeUsage(), acts: session.takeActs() } : {}),
252
256
  };
253
257
  }
@@ -257,15 +261,19 @@ export class CodingRuntime {
257
261
  switch (action.kind) {
258
262
  case "message": {
259
263
  const inputId = typeof action.inputId === "string" && action.inputId.length <= 128 ? action.inputId : undefined;
260
- // The same input twice is a retry after a lost reply, not a second instruction (#1019).
261
- if (inputId && session.acceptedInputs.includes(inputId))
262
- return { ...this.snapshot(sessionId), duplicateInput: inputId };
264
+ // The same input twice is a retry after a lost reply, not a second instruction (#1019) — but only with
265
+ // the same content: a changed payload under an accepted id is refused, never credited as the first.
266
+ const digest = actDigest(action);
267
+ const seen = inputId ? session.inputs.check(inputId, digest) : "new";
268
+ if (seen !== "new")
269
+ return { ...this.snapshot(sessionId), ...(seen === "duplicate" ? { duplicateInput: inputId } : { inputConflict: inputId }) };
263
270
  // Narrowed rather than trusted: this runner is a published package any caller can
264
271
  // POST to, and an unrecognised author must read as "unstated", not become a label.
265
272
  session.input(action.text, {
266
273
  author: asTurnAuthor(action.author),
267
274
  replay: typeof action.replay === "string" ? action.replay : undefined,
268
275
  inputId,
276
+ digest,
269
277
  objective: runObjective(action.objective),
270
278
  });
271
279
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proagentstore/cli",
3
- "version": "0.4.105",
3
+ "version": "0.4.106",
4
4
  "description": "CLI for creating, publishing, and running ProAgentStore agents",
5
5
  "license": "MIT",
6
6
  "type": "module",