@lazyingart/agintiflow 0.20.198 → 0.20.200

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 CHANGED
@@ -178,6 +178,13 @@ aginti --language de
178
178
  | Resume current project | `aginti resume` (`1` is newest/latest; Space shows more) |
179
179
  | Browse all sessions | `aginti resume --all-sessions` |
180
180
  | Queue into a running session | `aginti queue <session-id> "extra instruction"` |
181
+ | Inspect durable goal revisions | `aginti sessions list` and `aginti sessions show <session-id>` |
182
+
183
+ The runtime uses durable goal lifecycles, safe-boundary inbox interruption,
184
+ progressive tool/context disclosure, bounded read batching, evidence-gated
185
+ completion, and provider attribution. See
186
+ [State-of-the-Art Agent Runtime](docs/state-of-the-art-agent-runtime.md) for the
187
+ embedding and acceptance contract.
181
188
  | Clean empty sessions | `aginti --remove-empty-sessions` |
182
189
  | Check capabilities | `aginti capabilities`, `aginti doctor --capabilities` |
183
190
  | Sync reviewed skills | `aginti skillmesh status`, `aginti skillmesh sync` |
package/bin/aginti-cli.js CHANGED
@@ -1,7 +1,22 @@
1
1
  #!/usr/bin/env node
2
- import { main } from "../src/cli.js";
2
+ import fs from "node:fs";
3
3
 
4
- main().catch((error) => {
4
+ const argv = process.argv.slice(2);
5
+
6
+ function fail(error) {
5
7
  console.error(error);
6
8
  process.exit(1);
7
- });
9
+ }
10
+
11
+ try {
12
+ if (["--version", "version", "-v"].includes(argv[0])) {
13
+ const packageJson = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
14
+ console.log(packageJson.version);
15
+ } else {
16
+ import("../src/cli.js")
17
+ .then(({ main }) => main(argv))
18
+ .catch(fail);
19
+ }
20
+ } catch (error) {
21
+ fail(error);
22
+ }
@@ -8,8 +8,56 @@ AgInTiFlow keeps CLI and web runs equivalent by using a project-local session in
8
8
  - Canonical session store: `~/.agintiflow/sessions/<session-id>/`.
9
9
  - Runtime inbox: `~/.agintiflow/sessions/<session-id>/inbox.jsonl`.
10
10
 
11
+ ## Persistence Guarantees
12
+
13
+ Long-lived processes reuse one SQLite session-index connection and prepared
14
+ statement set per resolved `AGINTIFLOW_HOME`. The index enables WAL mode and a
15
+ bounded busy timeout so CLI, web, and bridge processes can update the
16
+ rebuildable index without repeatedly reopening and migrating the database.
17
+ Call `closeSessionIndexConnections()` in tests or embedding hosts that switch
18
+ runtime homes inside one process.
19
+
20
+ Each `SessionStore` memoizes directory/pointer initialization and serializes
21
+ its event appends. Concurrent callers therefore retain call order without
22
+ recreating the session pointer for every event. `state.json` remains an atomic,
23
+ fsynced save boundary. A missing state file is resumably absent; malformed JSON
24
+ raises `SESSION_STATE_CORRUPT` instead of silently looking like a new session.
25
+
26
+ ## Machine Run Input
27
+
28
+ `aginti run` uses deterministic input precedence: explicit `--stdin` reads
29
+ standard input, otherwise a positional prompt wins, and piped standard input is
30
+ used only when no positional prompt exists. This keeps positional automation
31
+ working in subprocesses whose stdin is non-interactive while preserving both
32
+ explicit and implicit pipe workflows. `aginti --version` is handled by the
33
+ lightweight launcher without loading the full agent and web runtime.
34
+
11
35
  When a run is active, the web chat and `aginti queue <session-id> "..."` append messages to the inbox instead of trying to mutate the running process directly. The web API exposes `GET /api/sessions/:id/inbox`, `POST /api/sessions/:id/inbox`, `PATCH /api/sessions/:id/inbox/:itemId`, and `DELETE /api/sessions/:id/inbox/:itemId` so browser users can inspect, edit, or remove pending pipe messages before the runner consumes them. The runner drains the inbox at safe boundaries: before each model step and after tool execution. This mirrors the event-queue style used by mature agent UIs while keeping the backend decoupled from any specific frontend.
12
36
 
37
+ Embedding hosts can continue the same durable context without scraping terminal
38
+ output:
39
+
40
+ ```bash
41
+ printf '%s' 'Follow up using the existing evidence.' \
42
+ | aginti resume SESSION_ID --stdin --json
43
+ ```
44
+
45
+ Machine resume emits the same single JSON object as `aginti run --json`, keeps
46
+ the original session ID, restores the saved provider/model/tool/runtime policy,
47
+ and never starts the web UI or writes interactive status lines. Explicit resume
48
+ options may still patch durable runtime fields, but `--stdin` and `--json` are
49
+ transport flags and are not persisted. This is the supported boundary for
50
+ LabCanvas, chat bridges, schedulers, and other embedding hosts that need one
51
+ isolated reusable agent session per conversation.
52
+
53
+ The object includes `goalRevision` and `goalStatus`. A first request starts at
54
+ revision 1. Every nonempty resume prompt advances the revision while retaining
55
+ the bounded prior-goal ledger. Accepted completion marks that revision
56
+ `completed`; a safe interruption or exhausted bounded run marks it `paused`;
57
+ provider/runtime failure marks it `failed`. The next continuation reactivates
58
+ the same session instead of replaying prior work. `aginti sessions show
59
+ SESSION_ID` exposes both the revision history and lifecycle transitions.
60
+
13
61
  The interactive CLI keeps the input panel visible while a run is working. Enter sends the current draft as an ASAP pipe message and displays it as `→`; the runner drains those messages before normal inbox items and before after-finish queued prompts. Tab stores the draft as an after-finish queue item and displays it as `↳`; those prompts run only after the current run completes. Alt+Up moves the last pending `→` message back into the editor, and Shift+Left moves the last pending `↳` message back into the editor. Idle Esc is ignored so it does not redraw the prompt into the transcript. During a run, Esc waits when `→` pipe messages are still pending and stops the run only when no ASAP pipe message is pending; Ctrl+C always stops. The current command cwd is rendered below the input panel in both idle and running states.
14
62
 
15
63
  The web UI uses a related but browser-appropriate pattern. Enter sends and Shift+Enter adds a newline. `Pipe to run` writes an ASAP inbox item shared with CLI. `Queue after finish` keeps a browser-local next prompt and starts it after the current web-owned run finishes. Both lanes render in a pending panel with Edit and Remove buttons instead of terminal-only keybindings.
@@ -0,0 +1,46 @@
1
+ # LabCanvas ChatOps Fallback
2
+
3
+ AgInTiFlow is a reasoning and tool-supervision fallback for LabCanvas. LabCanvas remains the owner of chat transport, schedules, exact-source media resolution, durable task state, routine selection, artifact validation, and delivery.
4
+
5
+ ## Contract
6
+
7
+ LabCanvas should pass one bounded task packet containing:
8
+
9
+ - exact current request and source-chat identity;
10
+ - latest same-chat interruptions and a small amount of attributed context;
11
+ - one selected routine and its contract paths;
12
+ - current deterministic preflight and stage state;
13
+ - exact artifact directory and irreversible-action gates.
14
+
15
+ AgInTi should read `AGENTS.md` and the selected routine contract, then call established commands. It should not invent a second scheduler, publication pipeline, media downloader, CAD generator, or delivery mechanism.
16
+
17
+ The default provider chain is `deepseek,localllm`. Switching providers is safe only when the first provider failed before inference or tool execution. Never replay an unknown task failure or timeout on another provider because the first attempt may already have caused a side effect.
18
+
19
+ ## Evidence Scope
20
+
21
+ ChatOps prompts may include a trusted single-line marker:
22
+
23
+ ```text
24
+ AGINTI_EVIDENCE_SCOPE_JSON: {"mode":"chat-response","request":"Produce only the requested chat response."}
25
+ ```
26
+
27
+ or:
28
+
29
+ ```text
30
+ AGINTI_EVIDENCE_SCOPE_JSON: {"mode":"task","request":"Create the requested PDF from the supplied evidence."}
31
+ ```
32
+
33
+ For `chat-response`, ordinary conversation and routing do not require file or command evidence. For `task`, evidence requirements are inferred only from the exact request, not from surrounding wrapper prose. Artifact requests still require real artifacts.
34
+
35
+ ## Local Context Recovery
36
+
37
+ LocalLLM planning compacts oversized goals to a bounded head-and-tail representation. Runtime compaction retains the first request and latest interruptions. A `LocalContextBudgetError` triggers one compact-and-retry cycle at the same step and records private recovery events. It does not authorize replaying task side effects.
38
+
39
+ Validate with:
40
+
41
+ ```bash
42
+ npm run check
43
+ npm run smoke:context-budget-recovery
44
+ npm run smoke:truthful-completion
45
+ ```
46
+
@@ -0,0 +1,179 @@
1
+ # State-of-the-Art Agent Runtime
2
+
3
+ AgInTiFlow is designed as an embeddable agent runtime, not a prompt wrapper.
4
+ Its main job is to preserve intent, evidence, and tool progress while providers,
5
+ frontends, and long-running tasks change underneath it.
6
+
7
+ This architecture draws on proven patterns from durable agent systems,
8
+ including the append-only session and lifecycle ideas in
9
+ [`deepseek-ai/deepseek-harness`](https://github.com/deepseek-ai/deepseek-harness),
10
+ while retaining AgInTiFlow's provider-neutral tool and safety boundaries. The
11
+ implementation is local and does not import that repository as a runtime
12
+ dependency.
13
+
14
+ ## Runtime Invariants
15
+
16
+ 1. One logical conversation owns one durable session ID.
17
+ 2. A continuation resumes that session; it does not replay the task from the
18
+ beginning.
19
+ 3. The current user request is authoritative, but prior verified evidence and
20
+ unfinished requirements remain available.
21
+ 4. Tool side effects are accepted only through explicit, validated contracts.
22
+ 5. A final answer is successful only when its required evidence exists.
23
+ 6. Provider failure is not task success and is not silently converted into it.
24
+ 7. Provider handoff preserves the session and goal rather than duplicating
25
+ external work.
26
+
27
+ ## Durable Goal Contract
28
+
29
+ Every session stores a versioned goal contract in `state.json`:
30
+
31
+ - `revision`: advances for every resumed request;
32
+ - `currentHash` and `currentPreview`: identify the authoritative request
33
+ without placing private raw prompts in operational indexes;
34
+ - `history`: bounded revision history with previous-goal and plan hashes;
35
+ - `status`: `active`, `completed`, `paused`, or `failed`;
36
+ - `lifecycle`: bounded status transitions with reason and timestamp.
37
+
38
+ Accepted direct answers, evidence-backed assistant answers, and `finish` tool
39
+ calls mark the current revision completed. User interruption, step exhaustion,
40
+ or a repairable tool-contract stop pauses it. Provider timeout, preflight
41
+ failure, and unexpected runtime errors mark it failed. A new continuation
42
+ reactivates the same session at the next revision.
43
+
44
+ Inspect it without parsing terminal output:
45
+
46
+ ```bash
47
+ aginti sessions list
48
+ aginti sessions show SESSION_ID
49
+ ```
50
+
51
+ Machine responses also expose `goalRevision` and `goalStatus`.
52
+
53
+ ## Append-Only Evidence And Atomic State
54
+
55
+ The canonical session lives under
56
+ `~/.agintiflow/sessions/<session-id>/`. Atomic `state.json` snapshots hold the
57
+ resumable working state. Append-only JSONL events preserve lifecycle, tool,
58
+ model, evidence, inbox, and recovery facts. A project-local index points to the
59
+ canonical session without copying private history.
60
+
61
+ Malformed state is treated as corruption, not as a missing session. Event
62
+ appends are serialized, state saves are atomic and fsynced, and the SQLite
63
+ session index uses WAL plus a bounded busy timeout for multiple frontends.
64
+
65
+ ## Safe Interruption And Inbox
66
+
67
+ `aginti queue SESSION_ID "instruction"`, the CLI composer, and the web app all
68
+ write to the same durable inbox. The runner consumes messages only at safe
69
+ boundaries:
70
+
71
+ - before a model step;
72
+ - after a tool action;
73
+ - before accepting completion.
74
+
75
+ This permits an operator or chat bridge to correct, narrow, extend, or cancel
76
+ the current direction without mutating an in-flight model request or replaying
77
+ completed side effects. ASAP input and after-finish input remain distinct.
78
+
79
+ ## Progressive Context And Tool Disclosure
80
+
81
+ The first model turn receives a focused runtime contract, the current goal,
82
+ relevant project instructions, and only the skills selected for that request.
83
+ It does not receive an indiscriminate dump of every skill or workspace file.
84
+
85
+ Tool access is progressively disclosed:
86
+
87
+ - direct chat can answer without tools;
88
+ - unfamiliar repositories begin with bounded inspection and search;
89
+ - relevant routines are preferred over rebuilding mature workflows;
90
+ - a small batch of up to four independent read-only calls may run in one turn;
91
+ - writes, GUI actions, network writes, and irreversible operations remain
92
+ isolated, ordered calls.
93
+
94
+ Workspace search is bounded by file count, bytes, and elapsed time. Default
95
+ root scans skip generated outputs, artifacts, and private data, while an
96
+ explicit path remains inspectable when the user actually requested it.
97
+
98
+ ## Truthful Completion
99
+
100
+ Completion is checked against an evidence scope. Read-only answers and plans do
101
+ not need irrelevant command or visual evidence. File creation, publication,
102
+ GUI work, and other external actions require the corresponding evidence.
103
+
104
+ The runtime provides:
105
+
106
+ - one bounded retry when a model claims completion without required evidence;
107
+ - one bounded repair for an empty model answer;
108
+ - a concise fallback only when runtime evidence already verifies completion;
109
+ - a resumable stop instead of a false success when evidence remains absent;
110
+ - short-circuiting after a blocked tool so later calls in that batch are not
111
+ dispatched against an invalid state.
112
+
113
+ ## Provider Attribution And Handoff
114
+
115
+ `npm run eval:provider-attribution` compares a raw provider answer with the
116
+ same provider through AgInTiFlow. Results are classified as:
117
+
118
+ - `both_pass`: provider and orchestration both satisfy the contract;
119
+ - `orchestration_loss_or_help`: the runtime changed the outcome, so inspect
120
+ prompting, context, tools, or completion gates;
121
+ - `provider_limit`: the raw model itself failed the contract.
122
+
123
+ This prevents orchestration bugs from being blamed on DeepSeek or a local
124
+ model, and prevents weak model output from triggering unnecessary framework
125
+ rewrites.
126
+
127
+ Embedding hosts should use a provider chain while preserving the same AgInTi
128
+ session. LabCanvas uses DeepSeek first and LocalLLM second. A handoff occurs
129
+ only after a categorized provider failure and must not replay verified side
130
+ effects. Codex and Claude remain explicit opt-in backends rather than hidden
131
+ fallbacks.
132
+
133
+ ## Machine Host Protocol
134
+
135
+ The supported subprocess boundary is:
136
+
137
+ ```bash
138
+ printf '%s' 'Do the task.' | aginti run --stdin --json [runtime options]
139
+ printf '%s' 'Continue with this correction.' \
140
+ | aginti resume SESSION_ID --stdin --json
141
+ ```
142
+
143
+ Exactly one JSON object is emitted. It includes success/failure, session,
144
+ provider, model, resume state, goal revision/status, result, stop state, and
145
+ reason. Interactive banners, update checks, and web startup are suppressed.
146
+ Stopped or failed runs always have `ok: false` even when they contain a useful
147
+ human-facing explanation.
148
+
149
+ ## Established Routines, Not Reinvention
150
+
151
+ AgInTiFlow is the reasoning and supervision layer. Domain work should use the
152
+ existing routine owned by the relevant project:
153
+
154
+ - LazyEdit and AutoPublish for subtitle-aware video publication;
155
+ - LALACHAN/Xiaoyunque for story and video generation;
156
+ - Musia for music and song-first MV workflows;
157
+ - LabCanvas CAD, KiCad, Blender, TeX/PDF, presentation, grant, and figure
158
+ routines;
159
+ - WeChat and WeCom transports for exact-chat delivery.
160
+
161
+ The agent selects, invokes, monitors, and verifies those routines. It does not
162
+ replace them with prompt-specific shell fragments.
163
+
164
+ ## Acceptance Gates
165
+
166
+ A primary-backend release is accepted only after all of these pass:
167
+
168
+ 1. Syntax and deterministic runtime tests.
169
+ 2. Durable run/resume, goal lifecycle, inbox, and session-isolation tests.
170
+ 3. Progressive tool, bounded search, and truthful-completion tests.
171
+ 4. Raw-provider versus agent attribution for DeepSeek and LocalLLM.
172
+ 5. A live read-only established-routine task with no accidental write.
173
+ 6. A live exact artifact-creation task with byte-level verification.
174
+ 7. A live LocalLLM direct-response task.
175
+ 8. Host project tests, chat-bridge self-tests, package dry run, installed
176
+ version check, and existing-runtime restart.
177
+
178
+ These gates keep the architecture fast for ordinary chat, capable for long
179
+ tasks, and honest when a provider or external service is unavailable.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.20.198",
3
+ "version": "0.20.200",
4
4
  "type": "module",
5
5
  "description": "AgInTiFlow is a project-aware agent workspace for hybrid wet-dry R&D, hardware-aware intelligence, software automation, and industrial workflows.",
6
6
  "license": "Apache-2.0",
@@ -86,6 +86,7 @@
86
86
  "scripts/local-first-agent-eval.mjs",
87
87
  "scripts/fixtures/local-first-agent-eval-fixtures.mjs",
88
88
  "scripts/smoke-run-stdin.js",
89
+ "scripts/smoke-runtime-core.js",
89
90
  "scripts/smoke-mcp.js",
90
91
  "scripts/fixtures/mcp-stdio-smoke-server.mjs",
91
92
  "scripts/smoke-model-roles.js",
@@ -121,7 +122,7 @@
121
122
  "scripts": {
122
123
  "start": "node run.js",
123
124
  "web": "node web.js",
124
- "check": "node --check run.js && node --check web.js && node --check bin/aginti-cli.js && node --check bin/aginti-public-research.js && node --check bin/aginti-safe-chat.js && node --check src/*.js && node --check src/mcp/*.js && node --check public/*.js && node --check scripts/postinstall-webapp.js && node --check scripts/seed-supervised-homework.js && node --check scripts/smoke-agentlink.js && node --check scripts/smoke-execution-policy.js && node --check scripts/smoke-math-rendering.js && node --check scripts/smoke-mcp.js && node --check scripts/smoke-public-research-wrapper.js && node --check scripts/smoke-safe-chat.js && node --check scripts/smoke-web-ui.js && node --check scripts/smoke-scs-evidence-visibility.js && node --check scripts/fixtures/mcp-stdio-smoke-server.mjs",
125
+ "check": "node --check run.js && node --check web.js && node --check bin/aginti-cli.js && node --check bin/aginti-public-research.js && node --check bin/aginti-safe-chat.js && node --check src/*.js && node --check src/mcp/*.js && node --check public/*.js && node --check scripts/postinstall-webapp.js && node --check scripts/seed-supervised-homework.js && node --check scripts/eval-provider-attribution.js && node --check scripts/smoke-agentlink.js && node --check scripts/smoke-execution-policy.js && node --check scripts/smoke-math-rendering.js && node --check scripts/smoke-mcp.js && node --check scripts/smoke-public-research-wrapper.js && node --check scripts/smoke-runtime-core.js && node --check scripts/smoke-safe-chat.js && node --check scripts/smoke-web-ui.js && node --check scripts/smoke-scs-evidence-visibility.js && node --check scripts/fixtures/mcp-stdio-smoke-server.mjs",
125
126
  "setup:toolchain-docker": "scripts/setup-agent-toolchain-docker.sh",
126
127
  "smoke:coding-tools": "node scripts/smoke-coding-tools.js",
127
128
  "smoke:dynamic-step-budget": "node scripts/smoke-dynamic-step-budget.js",
@@ -140,8 +141,10 @@
140
141
  "smoke:inbox": "node scripts/smoke-inbox.js",
141
142
  "smoke:long-jobs": "node scripts/smoke-long-jobs.js",
142
143
  "smoke:run-stdin": "node scripts/smoke-run-stdin.js",
144
+ "smoke:runtime-core": "node scripts/smoke-runtime-core.js",
143
145
  "smoke:localllm-auto-max": "node scripts/smoke-localllm-auto-max.js",
144
146
  "smoke:local-resource-policy": "node scripts/smoke-local-resource-policy.js",
147
+ "smoke:context-budget-recovery": "node scripts/smoke-context-budget-recovery.js",
145
148
  "smoke:localllm-model-tiers": "node scripts/smoke-localllm-model-tiers.js",
146
149
  "smoke:localllm-provider": "node scripts/smoke-localllm-provider.js",
147
150
  "smoke:progressive-tools": "node scripts/smoke-progressive-tool-selection.js",
@@ -165,13 +168,14 @@
165
168
  "smoke:web-port-fallback": "node scripts/smoke-web-port-fallback.js",
166
169
  "smoke:autoupdate": "node scripts/smoke-auto-update.js",
167
170
  "eval:local-first-agent": "node scripts/local-first-agent-eval.mjs",
171
+ "eval:provider-attribution": "node scripts/eval-provider-attribution.js",
168
172
  "real:deepseek": "node scripts/real-deepseek-capabilities.js",
169
173
  "postinstall": "node scripts/postinstall-webapp.js",
170
174
  "supervision:seed": "node scripts/seed-supervised-homework.js",
171
175
  "storage:migrate": "node bin/aginti-cli.js storage migrate",
172
176
  "publish:env": "node scripts/npm-publish-from-env.js publish --access public",
173
177
  "publish:env:whoami": "node scripts/npm-publish-from-env.js whoami",
174
- "test": "npm run check && npm run smoke:localllm-provider && npm run smoke:localllm-model-tiers && npm run smoke:localllm-auto-max && npm run smoke:local-resource-policy && npm run smoke:session-runtime && npm run smoke:progressive-tools && npm run smoke:truthful-completion && npm run smoke:writing-specialist-routing && npm run eval:local-first-agent && npm run smoke:runtime-compat && npm run smoke:autoupdate && npm run smoke:web-api && npm run smoke:math-rendering && npm run smoke:web-ui && npm run smoke:web-autostart && npm run smoke:webapp-command && npm run smoke:web-port-fallback && npm run smoke:docker-command && npm run smoke:coding-tools && npm run smoke:dynamic-step-budget && npm run smoke:execution-policy && npm run smoke:aaps-adapter && npm run smoke:auxiliary-tools && npm run smoke:perception-research && npm run smoke:public-research && npm run smoke:safe-chat && npm run smoke:auth && npm run smoke:agentlink && npm run smoke:canvas-artifacts && npm run smoke:capabilities && npm run smoke:mcp && npm run smoke:model-roles && npm run smoke:platform && npm run smoke:permission-modes && npm run smoke:skills && npm run smoke:skillmesh && npm run smoke:tmux-tools && npm run smoke:long-jobs && npm run smoke:run-stdin && npm run smoke:cli-chat && npm run smoke:inbox",
178
+ "test": "npm run check && npm run smoke:localllm-provider && npm run smoke:localllm-model-tiers && npm run smoke:localllm-auto-max && npm run smoke:local-resource-policy && npm run smoke:context-budget-recovery && npm run smoke:session-runtime && npm run smoke:runtime-core && npm run smoke:progressive-tools && npm run smoke:truthful-completion && npm run smoke:writing-specialist-routing && npm run eval:local-first-agent && npm run eval:provider-attribution && npm run smoke:runtime-compat && npm run smoke:autoupdate && npm run smoke:web-api && npm run smoke:math-rendering && npm run smoke:web-ui && npm run smoke:web-autostart && npm run smoke:webapp-command && npm run smoke:web-port-fallback && npm run smoke:docker-command && npm run smoke:coding-tools && npm run smoke:dynamic-step-budget && npm run smoke:execution-policy && npm run smoke:aaps-adapter && npm run smoke:auxiliary-tools && npm run smoke:perception-research && npm run smoke:public-research && npm run smoke:safe-chat && npm run smoke:auth && npm run smoke:agentlink && npm run smoke:canvas-artifacts && npm run smoke:capabilities && npm run smoke:mcp && npm run smoke:model-roles && npm run smoke:platform && npm run smoke:permission-modes && npm run smoke:skills && npm run smoke:skillmesh && npm run smoke:tmux-tools && npm run smoke:long-jobs && npm run smoke:run-stdin && npm run smoke:cli-chat && npm run smoke:inbox",
175
179
  "pack:dry-run": "npm pack --dry-run",
176
180
  "smoke:capabilities": "node scripts/smoke-capabilities.js"
177
181
  },
@@ -918,6 +918,23 @@ try {
918
918
  ) {
919
919
  throw new Error("resume subcommand options after the session id should remain options, not prompt text");
920
920
  }
921
+ const machineResume = parseResumeCommandArgs([
922
+ "web-agent-smoke",
923
+ "--stdin",
924
+ "--json",
925
+ "--provider",
926
+ "mock",
927
+ ]);
928
+ if (
929
+ machineResume.sessionId !== "web-agent-smoke" ||
930
+ !machineResume.stdin ||
931
+ !machineResume.json ||
932
+ machineResume.optionArgv.includes("--stdin") ||
933
+ machineResume.optionArgv.includes("--json") ||
934
+ parseArgs(machineResume.optionArgv).provider !== "mock"
935
+ ) {
936
+ throw new Error("resume machine transport flags should be parsed outside the durable runtime patch");
937
+ }
921
938
  const resumeRuntimeOptions = [
922
939
  "--provider",
923
940
  "mock",
@@ -18,6 +18,7 @@ import { SessionStore } from "../src/session-store.js";
18
18
  import {
19
19
  attachToolContract,
20
20
  createToolContract,
21
+ safeSequentialToolBatchLimit,
21
22
  toolContractFromResponse,
22
23
  validateToolCallBatch,
23
24
  } from "../src/tool-contract.js";
@@ -816,6 +817,81 @@ assert(
816
817
  "valid offered tool call did not satisfy its exact schema"
817
818
  );
818
819
 
820
+ const safeReadDescriptors = [
821
+ {
822
+ type: "function",
823
+ function: {
824
+ name: "read_file",
825
+ description: "Read a file.",
826
+ parameters: {
827
+ type: "object",
828
+ properties: { path: { type: "string" } },
829
+ required: ["path"],
830
+ additionalProperties: false,
831
+ },
832
+ },
833
+ },
834
+ {
835
+ type: "function",
836
+ function: {
837
+ name: "search_files",
838
+ description: "Search files.",
839
+ parameters: {
840
+ type: "object",
841
+ properties: { path: { type: "string" }, query: { type: "string" } },
842
+ required: ["path", "query"],
843
+ additionalProperties: false,
844
+ },
845
+ },
846
+ },
847
+ {
848
+ type: "function",
849
+ function: {
850
+ name: "list_files",
851
+ description: "List files.",
852
+ parameters: {
853
+ type: "object",
854
+ properties: { path: { type: "string" } },
855
+ required: ["path"],
856
+ additionalProperties: false,
857
+ },
858
+ },
859
+ },
860
+ ];
861
+ const safeReadCalls = [
862
+ contractCall("safe-read", "read_file", { path: "AGENTS.md" }),
863
+ contractCall("safe-search", "search_files", { path: ".", query: "lazyedit" }),
864
+ contractCall("safe-list", "list_files", { path: "." }),
865
+ ];
866
+ const safeReadContract = createToolContract(safeReadDescriptors);
867
+ assert(safeSequentialToolBatchLimit(safeReadCalls) === 4, "safe read batch did not receive the bounded sequential allowance");
868
+ assert(
869
+ validateToolCallBatch(safeReadCalls, safeReadContract, {
870
+ maxToolCalls: safeSequentialToolBatchLimit(safeReadCalls),
871
+ }).ok,
872
+ "valid safe read batch did not pass the exact per-turn contract"
873
+ );
874
+ const mixedReadWriteCalls = [
875
+ safeReadCalls[0],
876
+ contractCall("unsafe-write", "write_file", { path: "blocked.txt", content: "bad" }),
877
+ ];
878
+ assert(safeSequentialToolBatchLimit(mixedReadWriteCalls) === 1, "mixed read/write batch escaped the single-call limit");
879
+ assert(
880
+ !validateToolCallBatch(mixedReadWriteCalls, createToolContract([...safeReadDescriptors, strictWriteDescriptor]), {
881
+ maxToolCalls: safeSequentialToolBatchLimit(mixedReadWriteCalls),
882
+ }).ok,
883
+ "mixed read/write batch unexpectedly passed"
884
+ );
885
+ const oversizedReadCalls = Array.from({ length: 5 }, (_, index) =>
886
+ contractCall(`read-${index}`, "read_file", { path: `file-${index}.txt` })
887
+ );
888
+ assert(
889
+ !validateToolCallBatch(oversizedReadCalls, safeReadContract, {
890
+ maxToolCalls: safeSequentialToolBatchLimit(oversizedReadCalls),
891
+ }).ok,
892
+ "oversized read batch escaped the bounded allowance"
893
+ );
894
+
819
895
  for (const [label, call, expectedCode] of [
820
896
  [
821
897
  "hidden dryRun",