@themoltnet/agent-daemon 0.6.1 → 0.7.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/README.md +191 -0
- package/dist/main.js +768 -32
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -105,6 +105,197 @@ If any of the GitHub-side writes fails (rate limit, missing `gh`, network
|
|
|
105
105
|
blip, …) the daemon logs and continues — the other anchors are
|
|
106
106
|
independent and at least one usually survives.
|
|
107
107
|
|
|
108
|
+
## Local development & smoke testing
|
|
109
|
+
|
|
110
|
+
End-to-end smoke test of the daemon against a local Docker stack. Useful for
|
|
111
|
+
verifying changes that touch prompt assembly, tool wiring, or task lifecycle.
|
|
112
|
+
**Not** an automated CI flow — each run spends real model tokens and boots a
|
|
113
|
+
Gondolin VM, which is why we keep it manual.
|
|
114
|
+
|
|
115
|
+
### Prerequisites
|
|
116
|
+
|
|
117
|
+
- Docker running.
|
|
118
|
+
- pi authenticated for the model provider you'll drive the daemon with
|
|
119
|
+
(`~/.pi/agent/auth.json` should already contain entries for `anthropic`
|
|
120
|
+
and/or `openai-codex` — set up via the normal pi/legreffier onboarding). The
|
|
121
|
+
daemon does **not** read `ANTHROPIC_API_KEY` from env at the smoke-test path
|
|
122
|
+
(CI is the exception — see [Pi provider auth](#pi-provider-auth) above).
|
|
123
|
+
- `ssh-keygen` on `PATH`.
|
|
124
|
+
- A `sandbox.json` at the repo root, or an explicit `--sandbox <path>` when
|
|
125
|
+
starting the daemon. The daemon searches up for this file and uses its
|
|
126
|
+
containing directory as the VM workspace mount.
|
|
127
|
+
|
|
128
|
+
For `themoltnet`, prefer the checked-in repo `sandbox.json` as-is — it carries
|
|
129
|
+
the current pnpm/VFS workaround. A minimal `sandbox.json` for another repo:
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"hostExec": {
|
|
134
|
+
"autoApprove": [
|
|
135
|
+
{
|
|
136
|
+
"argsExcludes": ["--mirror", "--all", "--tags"],
|
|
137
|
+
"argsPrefix": ["push"],
|
|
138
|
+
"executable": "git"
|
|
139
|
+
}
|
|
140
|
+
]
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
That's only a starting point. `vfs.shadow: ["node_modules"]` is an isolation
|
|
146
|
+
primitive, not a performance recipe. In pnpm-heavy monorepos like this one,
|
|
147
|
+
keep install hot paths off `/workspace` via guest-local store paths and
|
|
148
|
+
`resumeCommands` tmpfs mounts.
|
|
149
|
+
|
|
150
|
+
### 1. Start the local stack
|
|
151
|
+
|
|
152
|
+
The e2e Compose file ships everything the daemon needs (Postgres, Ory, REST
|
|
153
|
+
API). Run from the **main repo root** — `docker compose` looks for
|
|
154
|
+
`.env.local` next to the compose file:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
cd <repo-root>
|
|
158
|
+
COMPOSE_DISABLE_ENV_FILE=true \
|
|
159
|
+
docker compose -f docker-compose.e2e.yaml up -d --build
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The REST API binds to **port 8080** (not 8000):
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
docker compose -f docker-compose.e2e.yaml ps rest-api
|
|
166
|
+
# ... 0.0.0.0:8080->8080/tcp ...
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
There is no `/_health` route mounted currently; once the container shows
|
|
170
|
+
`(healthy)` per `docker compose ps`, move on.
|
|
171
|
+
|
|
172
|
+
### 2. Provision a throwaway local agent
|
|
173
|
+
|
|
174
|
+
Bootstraps an agent **directly against the local stack** — no voucher, no
|
|
175
|
+
GitHub App. Writes `.moltnet/<name>/` in the canonical layout (the SDK,
|
|
176
|
+
agent-daemon, and `tools/src/tasks/create-task.ts` all consume the same
|
|
177
|
+
files).
|
|
178
|
+
|
|
179
|
+
> Run this from the worktree (or repo) where you want `.moltnet/<name>/` to
|
|
180
|
+
> live — that's also where you'll run the daemon.
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
# Source the local env so DATABASE_URL and ORY_*_URL are available.
|
|
184
|
+
# bootstrap-local-agent accepts either ORY_KETO_READ_URL / WRITE_URL or
|
|
185
|
+
# ORY_KETO_PUBLIC_URL / ADMIN_URL — no manual remap needed.
|
|
186
|
+
set -a; source <repo-root>/.env.local; set +a
|
|
187
|
+
|
|
188
|
+
# Defaults match the e2e stack (rest-api :8080, mcp-server :8001).
|
|
189
|
+
pnpm exec tsx tools/src/tasks/bootstrap-local-agent.ts --name local-dev
|
|
190
|
+
|
|
191
|
+
# Convenience: source the generated env file.
|
|
192
|
+
source .moltnet/local-dev/env
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
The script prints a JSON summary including the agent's identity, team id,
|
|
196
|
+
and private diary id.
|
|
197
|
+
|
|
198
|
+
> The bootstrapped agent has no GitHub App. That's fine for any task that
|
|
199
|
+
> doesn't touch `gh`. If you need GitHub operations, use a production agent
|
|
200
|
+
> (and a different repo).
|
|
201
|
+
|
|
202
|
+
### 3. Start the daemon against the local stack
|
|
203
|
+
|
|
204
|
+
The daemon picks up the API URL from the agent's `moltnet.json`. It is a
|
|
205
|
+
workspace package, not a global CLI — invoke it via `pnpm --filter`:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
pnpm --filter @themoltnet/agent-daemon dev poll \
|
|
209
|
+
--agent local-dev \
|
|
210
|
+
--team "$MOLTNET_TEAM_ID" \
|
|
211
|
+
--task-types fulfill_brief \
|
|
212
|
+
--provider openai-codex \
|
|
213
|
+
--model gpt-5.4-codex \
|
|
214
|
+
--debug
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
- If you're starting from a directory without `sandbox.json` at or above it,
|
|
218
|
+
pass `--sandbox <repo-root>/sandbox.json`.
|
|
219
|
+
- `--task-types fulfill_brief` scopes the queue. Omit to accept any
|
|
220
|
+
registered type.
|
|
221
|
+
- Pick provider/model that matches your pi auth credits. Common choices:
|
|
222
|
+
`--provider openai-codex --model gpt-5.4-codex`, or
|
|
223
|
+
`--provider anthropic --model claude-sonnet-4-6`.
|
|
224
|
+
- `dev` (= `tsx watch src/main.ts`) is fine for local. Use `cli` for a
|
|
225
|
+
one-shot run without watch.
|
|
226
|
+
|
|
227
|
+
Leave it running. It idles until a task lands in its queue.
|
|
228
|
+
|
|
229
|
+
### 4. Create a task
|
|
230
|
+
|
|
231
|
+
In another terminal, with `.moltnet/local-dev/env` sourced:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
pnpm exec tsx tools/src/tasks/create-task.ts \
|
|
235
|
+
--agent local-dev \
|
|
236
|
+
--task-file examples/tasks/api/fulfill-brief.create.template.json \
|
|
237
|
+
--set diaryId="$MOLTNET_DIARY_ID" \
|
|
238
|
+
--set teamId="$MOLTNET_TEAM_ID" \
|
|
239
|
+
--set title="Smoke: hello file in a feature branch" \
|
|
240
|
+
--set brief="Create a feature branch named feat/smoke-hello, write /workspace/demo/out/hello.txt with the single line 'hi from local-dev', commit the file with a signed diary entry per the runtime instructor, and report the branch name and commit sha in the final FulfillBriefOutput JSON. There is no remote to push to — leave pullRequestUrl null."
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
> **Why a real coding brief**: `fulfill_brief` requires the agent to emit a
|
|
244
|
+
> structured `FulfillBriefOutput` JSON
|
|
245
|
+
> (`{ branch, commits, pullRequestUrl, diaryEntryIds, summary }`) as its
|
|
246
|
+
> final message. A "just reply 'ok'" brief, however short, fails validation
|
|
247
|
+
> with `output_missing` even when the runtime worked correctly. Pick a task
|
|
248
|
+
> that fits the shape.
|
|
249
|
+
|
|
250
|
+
Watch the daemon logs and the diary:
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
moltnet entry list --diary-id "$MOLTNET_DIARY_ID" --limit 10 \
|
|
254
|
+
--credentials "$PWD/.moltnet/local-dev/moltnet.json"
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### What to verify
|
|
258
|
+
|
|
259
|
+
After the task completes, every entry produced **during the attempt** should:
|
|
260
|
+
|
|
261
|
+
- Live in `task.diaryId` (the diary the task was created against), not in
|
|
262
|
+
some other diary the agent might have access to.
|
|
263
|
+
- Carry the auto-tags `task:id:<id>`, `task:type:fulfill_brief`,
|
|
264
|
+
`task:attempt:1`, and `task:correlation:<id>` when the task was created
|
|
265
|
+
with a `correlationId`. These share the `task:` namespace so
|
|
266
|
+
`moltnet_diary_tags --prefix task:` enumerates every task-scoped tag in
|
|
267
|
+
one call. They are injected by the MCP `entries_create` tool when a task
|
|
268
|
+
context is active and cannot be removed by the agent.
|
|
269
|
+
|
|
270
|
+
### Cleanup
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
# Stop the daemon (Ctrl+C).
|
|
274
|
+
|
|
275
|
+
# Tear down the stack and discard the database.
|
|
276
|
+
COMPOSE_DISABLE_ENV_FILE=true \
|
|
277
|
+
docker compose -f docker-compose.e2e.yaml down -v
|
|
278
|
+
|
|
279
|
+
# Drop the local agent dir if you don't need it again.
|
|
280
|
+
rm -rf .moltnet/local-dev
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Re-running
|
|
284
|
+
|
|
285
|
+
`bootstrap-local-agent` refuses to overwrite an existing agent dir. Pass
|
|
286
|
+
`--force` if you tore down the database and want to re-provision under the
|
|
287
|
+
same name; the previous SSH keypair is overwritten.
|
|
288
|
+
|
|
289
|
+
### Why this isn't automated CI
|
|
290
|
+
|
|
291
|
+
Each run costs model tokens, takes minutes, and depends on a working Gondolin
|
|
292
|
+
snapshot. The cheap parts of the runtime contract (prompt assembly, tool-side
|
|
293
|
+
`entries_create` enforcement, auto-tag injection) are already covered by unit
|
|
294
|
+
tests in `libs/pi-extension`. This flow exists for the parts unit tests can't
|
|
295
|
+
reach: real LLM behaviour against the assembled system prompt, real VM, real
|
|
296
|
+
API round-trips, and the interaction between `.moltnet/<agent>/` identity
|
|
297
|
+
material and the active `sandbox.json`.
|
|
298
|
+
|
|
108
299
|
## License
|
|
109
300
|
|
|
110
301
|
AGPL-3.0-only.
|
package/dist/main.js
CHANGED
|
@@ -6,12 +6,13 @@ import { ROOT_CONTEXT, SpanStatusCode, context, metrics, propagation, trace } fr
|
|
|
6
6
|
import { pino, transport } from "pino";
|
|
7
7
|
import { readFile } from "node:fs/promises";
|
|
8
8
|
import { createHash as createHash$1 } from "node:crypto";
|
|
9
|
-
import path, { dirname, isAbsolute, join, resolve } from "node:path";
|
|
9
|
+
import path, { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
11
|
import { execFile, execFileSync } from "node:child_process";
|
|
12
12
|
import { DefaultResourceLoader, SessionManager, createAgentSession, createBashToolDefinition, createEditToolDefinition, createReadToolDefinition, createSyntheticSourceInfo, createWriteToolDefinition, defineTool, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
13
13
|
import { Type, getModel } from "@earendil-works/pi-ai";
|
|
14
14
|
import { MemoryProvider, RealFSProvider, ShadowProvider, VM, VmCheckpoint, createHttpHooks, createShadowPathPredicate, ensureImageSelector, loadGuestAssets } from "@earendil-works/gondolin";
|
|
15
|
+
import { DatabaseSync } from "node:sqlite";
|
|
15
16
|
import { once } from "node:events";
|
|
16
17
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
|
|
17
18
|
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
@@ -3037,22 +3038,6 @@ function validateRubricWeights(rubric) {
|
|
|
3037
3038
|
if (Math.abs(sum - 1) > 1e-6) return `Rubric weights must sum to 1.0 (got ${sum.toFixed(6)})`;
|
|
3038
3039
|
return null;
|
|
3039
3040
|
}
|
|
3040
|
-
`
|
|
3041
|
-
You are reviewing a GitHub pull request for **complexity** — how hard
|
|
3042
|
-
this change is to review safely, NOT whether it's correct or whether
|
|
3043
|
-
the feature is worthwhile. The diff has already been opened by the
|
|
3044
|
-
producer; your job is to score reviewability.
|
|
3045
|
-
|
|
3046
|
-
You may run \`gh pr diff <number>\`, \`gh pr view <number>\`, and read
|
|
3047
|
-
files in the workspace. Don't run tests, don't push commits, don't
|
|
3048
|
-
modify anything. The PR's GitHub URL is in the target metadata.
|
|
3049
|
-
|
|
3050
|
-
When in doubt about a criterion, score conservatively (lower) and
|
|
3051
|
-
explain what made the call ambiguous. Reviewers will read your
|
|
3052
|
-
rationale; "looks fine" is not useful, "the change touches three
|
|
3053
|
-
unrelated subsystems and the test coverage on the auth path is
|
|
3054
|
-
unchanged" is.
|
|
3055
|
-
`.trim();
|
|
3056
3041
|
//#endregion
|
|
3057
3042
|
//#region ../../libs/tasks/src/success-criteria.ts
|
|
3058
3043
|
/**
|
|
@@ -4772,6 +4757,10 @@ var BUILT_IN_TASK_TYPES = {
|
|
|
4772
4757
|
inputSchema: FulfillBriefInput,
|
|
4773
4758
|
outputSchema: FulfillBriefOutput,
|
|
4774
4759
|
outputKind: "artifact",
|
|
4760
|
+
resumable: true,
|
|
4761
|
+
workspaceMode: "dedicated_worktree",
|
|
4762
|
+
workspaceScope: "session",
|
|
4763
|
+
sessionScope: "correlation",
|
|
4775
4764
|
requiresReferences: false,
|
|
4776
4765
|
validateOutput: requireVerificationWhenCriteriaPresent
|
|
4777
4766
|
},
|
|
@@ -4780,6 +4769,9 @@ var BUILT_IN_TASK_TYPES = {
|
|
|
4780
4769
|
inputSchema: AssessBriefInput,
|
|
4781
4770
|
outputSchema: AssessBriefOutput,
|
|
4782
4771
|
outputKind: "judgment",
|
|
4772
|
+
workspaceMode: "dedicated_worktree",
|
|
4773
|
+
workspaceScope: "attempt",
|
|
4774
|
+
sessionScope: "none",
|
|
4783
4775
|
requiresReferences: true,
|
|
4784
4776
|
validateInput: validateJudgmentInput,
|
|
4785
4777
|
validateInputAsync: validateAssessBriefInputAsync
|
|
@@ -4789,6 +4781,8 @@ var BUILT_IN_TASK_TYPES = {
|
|
|
4789
4781
|
inputSchema: CuratePackInput,
|
|
4790
4782
|
outputSchema: CuratePackOutput,
|
|
4791
4783
|
outputKind: "artifact",
|
|
4784
|
+
workspaceScope: "attempt",
|
|
4785
|
+
sessionScope: "none",
|
|
4792
4786
|
requiresReferences: false,
|
|
4793
4787
|
validateOutput: requireVerificationWhenCriteriaPresent
|
|
4794
4788
|
},
|
|
@@ -4797,6 +4791,8 @@ var BUILT_IN_TASK_TYPES = {
|
|
|
4797
4791
|
inputSchema: RenderPackInput,
|
|
4798
4792
|
outputSchema: RenderPackOutput,
|
|
4799
4793
|
outputKind: "artifact",
|
|
4794
|
+
workspaceScope: "attempt",
|
|
4795
|
+
sessionScope: "none",
|
|
4800
4796
|
requiresReferences: false,
|
|
4801
4797
|
validateOutput: requireVerificationWhenCriteriaPresent,
|
|
4802
4798
|
validateInputAsync: validateRenderPackInputAsync
|
|
@@ -4806,6 +4802,8 @@ var BUILT_IN_TASK_TYPES = {
|
|
|
4806
4802
|
inputSchema: JudgePackInput,
|
|
4807
4803
|
outputSchema: JudgePackOutput,
|
|
4808
4804
|
outputKind: "judgment",
|
|
4805
|
+
workspaceScope: "attempt",
|
|
4806
|
+
sessionScope: "none",
|
|
4809
4807
|
requiresReferences: true,
|
|
4810
4808
|
validateInput: validateJudgmentInput,
|
|
4811
4809
|
validateOutput: validateJudgePackOutput,
|
|
@@ -4816,6 +4814,8 @@ var BUILT_IN_TASK_TYPES = {
|
|
|
4816
4814
|
inputSchema: RunEvalInput,
|
|
4817
4815
|
outputSchema: RunEvalOutput,
|
|
4818
4816
|
outputKind: "artifact",
|
|
4817
|
+
workspaceScope: "attempt",
|
|
4818
|
+
sessionScope: "custom",
|
|
4819
4819
|
requiresReferences: false,
|
|
4820
4820
|
validateOutput: validateRunEvalOutput
|
|
4821
4821
|
},
|
|
@@ -4824,6 +4824,8 @@ var BUILT_IN_TASK_TYPES = {
|
|
|
4824
4824
|
inputSchema: JudgeEvalVariantInput,
|
|
4825
4825
|
outputSchema: JudgeEvalVariantOutput,
|
|
4826
4826
|
outputKind: "judgment",
|
|
4827
|
+
workspaceScope: "attempt",
|
|
4828
|
+
sessionScope: "custom",
|
|
4827
4829
|
requiresReferences: false,
|
|
4828
4830
|
validateInput: validateJudgeEvalVariantInput,
|
|
4829
4831
|
validateOutput: validateJudgeEvalVariantOutput,
|
|
@@ -5737,6 +5739,58 @@ function getTaskOutputSchema(taskType) {
|
|
|
5737
5739
|
function taskTypeUsesSubagents(taskType) {
|
|
5738
5740
|
return getTaskTypeEntry(taskType)?.usesSubagents === true;
|
|
5739
5741
|
}
|
|
5742
|
+
/**
|
|
5743
|
+
* Filesystem isolation policy requested by the task type.
|
|
5744
|
+
*
|
|
5745
|
+
* Unknown task types and task types without an explicit policy default to the
|
|
5746
|
+
* legacy/shared behaviour.
|
|
5747
|
+
*/
|
|
5748
|
+
function taskTypeWorkspaceMode(taskType) {
|
|
5749
|
+
return getTaskTypeEntry(taskType)?.workspaceMode ?? "shared_mount";
|
|
5750
|
+
}
|
|
5751
|
+
/**
|
|
5752
|
+
* Whether the daemon may keep a warm executor session alive after one
|
|
5753
|
+
* attempt completes and reuse it for later related tasks.
|
|
5754
|
+
*
|
|
5755
|
+
* Unknown task types default to `false`.
|
|
5756
|
+
*/
|
|
5757
|
+
function taskTypeResumable(taskType) {
|
|
5758
|
+
return getTaskTypeEntry(taskType)?.resumable === true;
|
|
5759
|
+
}
|
|
5760
|
+
/**
|
|
5761
|
+
* Lifetime of the task type's workspace.
|
|
5762
|
+
*
|
|
5763
|
+
* Unknown task types and task types without an explicit policy default to the
|
|
5764
|
+
* disposable per-attempt behavior.
|
|
5765
|
+
*/
|
|
5766
|
+
function taskTypeWorkspaceScope(taskType) {
|
|
5767
|
+
return getTaskTypeEntry(taskType)?.workspaceScope ?? "attempt";
|
|
5768
|
+
}
|
|
5769
|
+
/**
|
|
5770
|
+
* Granularity of daemon-local warm-session reuse.
|
|
5771
|
+
*
|
|
5772
|
+
* Unknown task types and task types without an explicit policy default to no
|
|
5773
|
+
* reuse.
|
|
5774
|
+
*/
|
|
5775
|
+
function taskTypeSessionScope(taskType) {
|
|
5776
|
+
return getTaskTypeEntry(taskType)?.sessionScope ?? "none";
|
|
5777
|
+
}
|
|
5778
|
+
/**
|
|
5779
|
+
* Resolve the daemon-relevant execution policy declared by a task type.
|
|
5780
|
+
*
|
|
5781
|
+
* This is the single read path for "may this task reuse a warm session?" and
|
|
5782
|
+
* "does its workspace belong to the attempt or the session?" so daemon code
|
|
5783
|
+
* doesn't need to hand-roll defaults or peek at the raw registry shape.
|
|
5784
|
+
*/
|
|
5785
|
+
function getTaskExecutionPolicy(taskType) {
|
|
5786
|
+
return {
|
|
5787
|
+
resumable: taskTypeResumable(taskType),
|
|
5788
|
+
workspaceMode: taskTypeWorkspaceMode(taskType),
|
|
5789
|
+
workspaceScope: taskTypeWorkspaceScope(taskType),
|
|
5790
|
+
sessionScope: taskTypeSessionScope(taskType),
|
|
5791
|
+
usesSubagents: taskTypeUsesSubagents(taskType)
|
|
5792
|
+
};
|
|
5793
|
+
}
|
|
5740
5794
|
//#endregion
|
|
5741
5795
|
//#region ../../libs/tasks/src/wire.ts
|
|
5742
5796
|
/**
|
|
@@ -5999,6 +6053,10 @@ var COMMON_OPTIONAL_FLAGS = `\
|
|
|
5999
6053
|
this, the pi session aborts and the attempt
|
|
6000
6054
|
fails with code 'max_bash_timeouts_exceeded'.
|
|
6001
6055
|
0 = disabled. Default: 3.
|
|
6056
|
+
--warm-session-ttl-sec <n> Retain resumable daemon slots (Pi sessions +
|
|
6057
|
+
reusable worktrees) in local daemon state for
|
|
6058
|
+
this many seconds after use. 0 = disable reuse.
|
|
6059
|
+
Default: 1800.
|
|
6002
6060
|
--debug Verbose logging: also log successful list/claim
|
|
6003
6061
|
outcomes (candidate counts, claim attempts).`;
|
|
6004
6062
|
var REGISTERED_TASK_TYPES = Object.keys(BUILT_IN_TASK_TYPES).sort();
|
|
@@ -6362,6 +6420,15 @@ function buildAssessBriefUserPrompt(input, ctx) {
|
|
|
6362
6420
|
rubric.preamble,
|
|
6363
6421
|
""
|
|
6364
6422
|
].join("\n") : "";
|
|
6423
|
+
const workspaceSection = ctx.workspace?.mode === "dedicated_worktree" ? [
|
|
6424
|
+
"### Workspace",
|
|
6425
|
+
"",
|
|
6426
|
+
"This review attempt is running inside a dedicated disposable git",
|
|
6427
|
+
"worktree created for this task. If you need to check out the target",
|
|
6428
|
+
"branch or inspect refs locally, do it only inside this worktree.",
|
|
6429
|
+
ctx.workspace.branch ? `The current review branch is \`${ctx.workspace.branch}\`. You may replace it with the target branch locally if that helps your inspection.` : "The current checkout is disposable and will be cleaned up when the task ends.",
|
|
6430
|
+
""
|
|
6431
|
+
].join("\n") : "";
|
|
6365
6432
|
return [
|
|
6366
6433
|
"# Assess Brief Judge",
|
|
6367
6434
|
"",
|
|
@@ -6402,6 +6469,7 @@ function buildAssessBriefUserPrompt(input, ctx) {
|
|
|
6402
6469
|
" read it from the task you fetched in step 1 and pass",
|
|
6403
6470
|
" `taskFilter: { correlationId: \"<id>\" }`.",
|
|
6404
6471
|
"",
|
|
6472
|
+
workspaceSection,
|
|
6405
6473
|
preambleSection,
|
|
6406
6474
|
"## Criteria",
|
|
6407
6475
|
"",
|
|
@@ -6661,6 +6729,14 @@ function buildFulfillBriefUserPrompt(input, ctx) {
|
|
|
6661
6729
|
"from this branch naming scheme when correlationId is set.",
|
|
6662
6730
|
""
|
|
6663
6731
|
].join("\n") : "";
|
|
6732
|
+
const workspaceSection = ctx.workspace?.mode === "dedicated_worktree" ? [
|
|
6733
|
+
"### Workspace",
|
|
6734
|
+
"",
|
|
6735
|
+
"This attempt is running inside a dedicated git worktree created",
|
|
6736
|
+
"for this task. Do not repurpose or switch the primary checkout.",
|
|
6737
|
+
ctx.workspace.branch ? `The current branch is \`${ctx.workspace.branch}\`. Stay on this branch unless the runtime instructor explicitly tells you otherwise.` : "Stay on the branch that was pre-provisioned for this task.",
|
|
6738
|
+
""
|
|
6739
|
+
].join("\n") : "";
|
|
6664
6740
|
return [
|
|
6665
6741
|
"# Fulfill Brief Agent",
|
|
6666
6742
|
"",
|
|
@@ -6681,9 +6757,10 @@ function buildFulfillBriefUserPrompt(input, ctx) {
|
|
|
6681
6757
|
criteriaSection,
|
|
6682
6758
|
seedSection,
|
|
6683
6759
|
correlationSection,
|
|
6760
|
+
workspaceSection,
|
|
6684
6761
|
"### Workflow",
|
|
6685
6762
|
"",
|
|
6686
|
-
`1. Create a feature branch (starting prefix suggestion: \`${branchSlug}<short-slug>\`).`,
|
|
6763
|
+
ctx.workspace?.mode === "dedicated_worktree" ? `1. Use the already-provisioned dedicated worktree branch${ctx.workspace.branch ? ` (\`${ctx.workspace.branch}\`)` : ""}; do not create or switch the primary checkout.` : `1. Create a feature branch (starting prefix suggestion: \`${branchSlug}<short-slug>\`).`,
|
|
6687
6764
|
"2. Understand the problem — read relevant code; do not speculate.",
|
|
6688
6765
|
"3. Implement the change. Keep commits small and coherent.",
|
|
6689
6766
|
"4. Add tests if applicable.",
|
|
@@ -7081,7 +7158,8 @@ function buildTaskUserPrompt(task, ctx) {
|
|
|
7081
7158
|
return buildFulfillBriefUserPrompt(task.input, {
|
|
7082
7159
|
diaryId: ctx.diaryId,
|
|
7083
7160
|
taskId: ctx.taskId,
|
|
7084
|
-
correlationId: task.correlationId
|
|
7161
|
+
correlationId: task.correlationId,
|
|
7162
|
+
workspace: ctx.workspace
|
|
7085
7163
|
});
|
|
7086
7164
|
case ASSESS_BRIEF_TYPE:
|
|
7087
7165
|
if (!Check(AssessBriefInput, task.input)) {
|
|
@@ -7090,7 +7168,8 @@ function buildTaskUserPrompt(task, ctx) {
|
|
|
7090
7168
|
}
|
|
7091
7169
|
return buildAssessBriefUserPrompt(task.input, {
|
|
7092
7170
|
diaryId: ctx.diaryId,
|
|
7093
|
-
taskId: ctx.taskId
|
|
7171
|
+
taskId: ctx.taskId,
|
|
7172
|
+
workspace: ctx.workspace
|
|
7094
7173
|
});
|
|
7095
7174
|
case CURATE_PACK_TYPE:
|
|
7096
7175
|
if (!Check(CuratePackInput, task.input)) {
|
|
@@ -15313,9 +15392,11 @@ var NO_SKILLS = () => ({
|
|
|
15313
15392
|
diagnostics: []
|
|
15314
15393
|
});
|
|
15315
15394
|
/**
|
|
15316
|
-
* Construct an
|
|
15317
|
-
*
|
|
15318
|
-
*
|
|
15395
|
+
* Construct an `AgentSession`. By default it is in-memory; callers may opt
|
|
15396
|
+
* parent sessions into daemon-owned file persistence via `sessionPersistence`.
|
|
15397
|
+
* The caller is responsible for eventually invoking `session.prompt(...)` and
|
|
15398
|
+
* for tearing down — the helper does no lifecycle management beyond
|
|
15399
|
+
* construction.
|
|
15319
15400
|
*/
|
|
15320
15401
|
async function buildAgentSession(args) {
|
|
15321
15402
|
const piOtelExtension = createPiOtelExtension({
|
|
@@ -15330,15 +15411,23 @@ async function buildAgentSession(args) {
|
|
|
15330
15411
|
skillsOverride: args.skillsOverride ?? NO_SKILLS
|
|
15331
15412
|
});
|
|
15332
15413
|
await resourceLoader.reload();
|
|
15414
|
+
const sessionManager = args.sessionPersistence ? await resolvePersistentSessionManager({
|
|
15415
|
+
cwd: args.mountPath,
|
|
15416
|
+
sessionDir: args.sessionPersistence.sessionDir
|
|
15417
|
+
}) : SessionManager.inMemory(args.mountPath);
|
|
15333
15418
|
return (await createAgentSession({
|
|
15334
15419
|
agentDir: args.piAuthDir,
|
|
15335
15420
|
cwd: args.mountPath,
|
|
15336
15421
|
model: args.modelHandle,
|
|
15337
15422
|
customTools: args.customTools,
|
|
15338
|
-
sessionManager
|
|
15423
|
+
sessionManager,
|
|
15339
15424
|
resourceLoader
|
|
15340
15425
|
})).session;
|
|
15341
15426
|
}
|
|
15427
|
+
async function resolvePersistentSessionManager(args) {
|
|
15428
|
+
await SessionManager.list(args.cwd, args.sessionDir);
|
|
15429
|
+
return SessionManager.continueRecent(args.cwd, args.sessionDir);
|
|
15430
|
+
}
|
|
15342
15431
|
//#endregion
|
|
15343
15432
|
//#region ../../libs/pi-extension/src/runtime/inject-task-context.ts
|
|
15344
15433
|
/**
|
|
@@ -15949,6 +16038,113 @@ function resolveSubmitTools(taskType, opts = {}) {
|
|
|
15949
16038
|
};
|
|
15950
16039
|
}
|
|
15951
16040
|
//#endregion
|
|
16041
|
+
//#region ../../libs/pi-extension/src/runtime/task-workspace.ts
|
|
16042
|
+
function prepareTaskWorkspace(task, requestedMountPath, executionPlan) {
|
|
16043
|
+
const branch = executionPlan?.worktreeBranch ?? null;
|
|
16044
|
+
if (!branch) return {
|
|
16045
|
+
mountPath: requestedMountPath,
|
|
16046
|
+
mode: "shared_mount",
|
|
16047
|
+
branch: null,
|
|
16048
|
+
cleanup: () => {}
|
|
16049
|
+
};
|
|
16050
|
+
const mainRepo = findMainWorktree();
|
|
16051
|
+
const worktreeDir = resolveTaskWorktreePath(mainRepo, executionPlan?.workspaceId ?? `task-${task.id}`);
|
|
16052
|
+
const relMount = relative(mainRepo, requestedMountPath);
|
|
16053
|
+
const mountPath = relMount === "" || relMount.startsWith("..") ? worktreeDir : join(worktreeDir, relMount);
|
|
16054
|
+
const keepWorkspace = executionPlan?.workspaceScope === "session" && executionPlan.sessionKey !== null;
|
|
16055
|
+
if (keepWorkspace) ensureReusableTaskWorktree(mainRepo, worktreeDir, branch);
|
|
16056
|
+
else {
|
|
16057
|
+
removeExistingTaskWorktree(mainRepo, worktreeDir);
|
|
16058
|
+
addTaskWorktree(mainRepo, worktreeDir, branch);
|
|
16059
|
+
}
|
|
16060
|
+
return {
|
|
16061
|
+
mountPath,
|
|
16062
|
+
mode: "dedicated_worktree",
|
|
16063
|
+
branch,
|
|
16064
|
+
cleanup: keepWorkspace ? () => {} : () => {
|
|
16065
|
+
execFileSync("git", [
|
|
16066
|
+
"-C",
|
|
16067
|
+
mainRepo,
|
|
16068
|
+
"worktree",
|
|
16069
|
+
"remove",
|
|
16070
|
+
"--force",
|
|
16071
|
+
worktreeDir
|
|
16072
|
+
], { stdio: "pipe" });
|
|
16073
|
+
}
|
|
16074
|
+
};
|
|
16075
|
+
}
|
|
16076
|
+
function resolveTaskWorktreePath(mainRepo, workspaceId) {
|
|
16077
|
+
return join(mainRepo, ".worktrees", workspaceId);
|
|
16078
|
+
}
|
|
16079
|
+
function ensureReusableTaskWorktree(mainRepo, worktreeDir, branch) {
|
|
16080
|
+
if (isRegisteredWorktree$1(mainRepo, worktreeDir)) return;
|
|
16081
|
+
if (existsSync(worktreeDir)) throw new Error(`Expected reusable worktree ${worktreeDir} to be git-managed, but it exists outside git worktree metadata.`);
|
|
16082
|
+
addTaskWorktree(mainRepo, worktreeDir, branch);
|
|
16083
|
+
}
|
|
16084
|
+
function addTaskWorktree(mainRepo, worktreeDir, branch) {
|
|
16085
|
+
const baseRef = resolveWorktreeBaseRef(mainRepo);
|
|
16086
|
+
execFileSync("git", gitRefExists(mainRepo, `refs/heads/${branch}`) ? [
|
|
16087
|
+
"-C",
|
|
16088
|
+
mainRepo,
|
|
16089
|
+
"worktree",
|
|
16090
|
+
"add",
|
|
16091
|
+
worktreeDir,
|
|
16092
|
+
branch
|
|
16093
|
+
] : [
|
|
16094
|
+
"-C",
|
|
16095
|
+
mainRepo,
|
|
16096
|
+
"worktree",
|
|
16097
|
+
"add",
|
|
16098
|
+
"-b",
|
|
16099
|
+
branch,
|
|
16100
|
+
worktreeDir,
|
|
16101
|
+
baseRef
|
|
16102
|
+
], { stdio: "pipe" });
|
|
16103
|
+
}
|
|
16104
|
+
function removeExistingTaskWorktree(mainRepo, worktreeDir) {
|
|
16105
|
+
if (!existsSync(worktreeDir) || !isRegisteredWorktree$1(mainRepo, worktreeDir)) return;
|
|
16106
|
+
execFileSync("git", [
|
|
16107
|
+
"-C",
|
|
16108
|
+
mainRepo,
|
|
16109
|
+
"worktree",
|
|
16110
|
+
"remove",
|
|
16111
|
+
"--force",
|
|
16112
|
+
worktreeDir
|
|
16113
|
+
], { stdio: "pipe" });
|
|
16114
|
+
}
|
|
16115
|
+
function isRegisteredWorktree$1(mainRepo, worktreeDir) {
|
|
16116
|
+
const list = execFileSync("git", [
|
|
16117
|
+
"-C",
|
|
16118
|
+
mainRepo,
|
|
16119
|
+
"worktree",
|
|
16120
|
+
"list",
|
|
16121
|
+
"--porcelain"
|
|
16122
|
+
], {
|
|
16123
|
+
encoding: "utf8",
|
|
16124
|
+
stdio: "pipe"
|
|
16125
|
+
});
|
|
16126
|
+
const marker = `worktree ${worktreeDir}\n`;
|
|
16127
|
+
return list.includes(marker) || list.endsWith(`worktree ${worktreeDir}`);
|
|
16128
|
+
}
|
|
16129
|
+
function resolveWorktreeBaseRef(mainRepo) {
|
|
16130
|
+
return gitRefExists(mainRepo, "refs/heads/main") ? "main" : "HEAD";
|
|
16131
|
+
}
|
|
16132
|
+
function gitRefExists(mainRepo, ref) {
|
|
16133
|
+
try {
|
|
16134
|
+
execFileSync("git", [
|
|
16135
|
+
"-C",
|
|
16136
|
+
mainRepo,
|
|
16137
|
+
"show-ref",
|
|
16138
|
+
"--verify",
|
|
16139
|
+
"--quiet",
|
|
16140
|
+
ref
|
|
16141
|
+
], { stdio: "pipe" });
|
|
16142
|
+
return true;
|
|
16143
|
+
} catch {
|
|
16144
|
+
return false;
|
|
16145
|
+
}
|
|
16146
|
+
}
|
|
16147
|
+
//#endregion
|
|
15952
16148
|
//#region ../../libs/pi-extension/src/runtime/execute-pi-task.ts
|
|
15953
16149
|
/**
|
|
15954
16150
|
* executePiTask — run a single Task attempt using pi-coding-agent inside a
|
|
@@ -15997,7 +16193,10 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
15997
16193
|
const task = claimedTask.task;
|
|
15998
16194
|
const attemptN = claimedTask.attemptN;
|
|
15999
16195
|
const startTime = Date.now();
|
|
16000
|
-
const
|
|
16196
|
+
const requestedMountPath = opts.mountPath ?? process.cwd();
|
|
16197
|
+
const executionPlan = opts.makeExecutionPlan?.(claimedTask) ?? null;
|
|
16198
|
+
const workspace = prepareTaskWorkspace(task, requestedMountPath, executionPlan);
|
|
16199
|
+
const mountPath = workspace.mountPath;
|
|
16001
16200
|
if (reporter.cancelSignal.aborted) return {
|
|
16002
16201
|
taskId: task.id,
|
|
16003
16202
|
attemptN,
|
|
@@ -16028,7 +16227,8 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
16028
16227
|
"--relative-paths"
|
|
16029
16228
|
], { stdio: "pipe" });
|
|
16030
16229
|
} catch {}
|
|
16031
|
-
|
|
16230
|
+
let managed = null;
|
|
16231
|
+
managed = await resumeVm({
|
|
16032
16232
|
checkpointPath,
|
|
16033
16233
|
agentName: opts.agentName,
|
|
16034
16234
|
mountPath,
|
|
@@ -16088,13 +16288,19 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
16088
16288
|
taskType: task.taskType,
|
|
16089
16289
|
teamId: task.teamId,
|
|
16090
16290
|
provider: opts.provider,
|
|
16091
|
-
model: opts.model
|
|
16291
|
+
model: opts.model,
|
|
16292
|
+
workspaceMode: workspace.mode,
|
|
16293
|
+
workspaceBranch: workspace.branch
|
|
16092
16294
|
});
|
|
16093
16295
|
let taskPrompt;
|
|
16094
16296
|
try {
|
|
16095
16297
|
taskPrompt = buildTaskUserPrompt(task, {
|
|
16096
16298
|
diaryId,
|
|
16097
16299
|
taskId: task.id,
|
|
16300
|
+
workspace: {
|
|
16301
|
+
mode: workspace.mode,
|
|
16302
|
+
branch: workspace.branch
|
|
16303
|
+
},
|
|
16098
16304
|
extras: opts.promptExtras
|
|
16099
16305
|
});
|
|
16100
16306
|
} catch (err) {
|
|
@@ -16205,7 +16411,8 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
16205
16411
|
"moltnet.task.id": task.id,
|
|
16206
16412
|
"moltnet.task.attempt": attemptN,
|
|
16207
16413
|
"moltnet.task.type": task.taskType
|
|
16208
|
-
}
|
|
16414
|
+
},
|
|
16415
|
+
sessionPersistence: executionPlan?.sessionPersistence ?? void 0
|
|
16209
16416
|
});
|
|
16210
16417
|
} catch (err) {
|
|
16211
16418
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -16424,7 +16631,13 @@ async function executePiTask(claimedTask, reporter, opts) {
|
|
|
16424
16631
|
console.error(`executePiTask: reporter.close() failed for task ${task.id} attempt ${attemptN}: ${detail}`);
|
|
16425
16632
|
}
|
|
16426
16633
|
}
|
|
16427
|
-
await managed.vm.close();
|
|
16634
|
+
if (managed) await managed.vm.close();
|
|
16635
|
+
try {
|
|
16636
|
+
workspace.cleanup();
|
|
16637
|
+
} catch (err) {
|
|
16638
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
16639
|
+
console.error(`executePiTask: workspace cleanup failed for task ${task.id} attempt ${attemptN}: ${detail}`);
|
|
16640
|
+
}
|
|
16428
16641
|
}
|
|
16429
16642
|
}
|
|
16430
16643
|
function emptyUsage(provider, model) {
|
|
@@ -16626,6 +16839,419 @@ function createGhCliClient() {
|
|
|
16626
16839
|
};
|
|
16627
16840
|
}
|
|
16628
16841
|
//#endregion
|
|
16842
|
+
//#region src/lib/daemon-slot-registry.ts
|
|
16843
|
+
var DaemonSlotRegistryError = class extends Error {
|
|
16844
|
+
constructor(operation, cause) {
|
|
16845
|
+
super(`Daemon slot registry failed during ${operation}`, { cause });
|
|
16846
|
+
this.operation = operation;
|
|
16847
|
+
this.name = "DaemonSlotRegistryError";
|
|
16848
|
+
}
|
|
16849
|
+
};
|
|
16850
|
+
var DaemonSlotRegistry = class {
|
|
16851
|
+
db;
|
|
16852
|
+
constructor(dbPath) {
|
|
16853
|
+
try {
|
|
16854
|
+
this.db = new DatabaseSync(dbPath);
|
|
16855
|
+
this.withDb("initialize schema", () => {
|
|
16856
|
+
this.db.exec(`
|
|
16857
|
+
PRAGMA journal_mode = WAL;
|
|
16858
|
+
|
|
16859
|
+
CREATE TABLE IF NOT EXISTS daemon_slots (
|
|
16860
|
+
agent_name TEXT NOT NULL,
|
|
16861
|
+
provider TEXT NOT NULL,
|
|
16862
|
+
model TEXT NOT NULL,
|
|
16863
|
+
slot_key TEXT NOT NULL,
|
|
16864
|
+
task_type TEXT NOT NULL,
|
|
16865
|
+
state TEXT NOT NULL CHECK (state IN ('active', 'idle')),
|
|
16866
|
+
last_task_id TEXT NOT NULL,
|
|
16867
|
+
last_attempt_n INTEGER NOT NULL,
|
|
16868
|
+
created_at_ms INTEGER NOT NULL,
|
|
16869
|
+
last_used_at_ms INTEGER NOT NULL,
|
|
16870
|
+
expires_at_ms INTEGER NOT NULL,
|
|
16871
|
+
PRIMARY KEY (agent_name, provider, model, slot_key)
|
|
16872
|
+
);
|
|
16873
|
+
|
|
16874
|
+
CREATE TABLE IF NOT EXISTS daemon_slot_sessions (
|
|
16875
|
+
agent_name TEXT NOT NULL,
|
|
16876
|
+
provider TEXT NOT NULL,
|
|
16877
|
+
model TEXT NOT NULL,
|
|
16878
|
+
slot_key TEXT NOT NULL,
|
|
16879
|
+
session_dir TEXT NOT NULL UNIQUE,
|
|
16880
|
+
session_path TEXT,
|
|
16881
|
+
PRIMARY KEY (agent_name, provider, model, slot_key),
|
|
16882
|
+
FOREIGN KEY (agent_name, provider, model, slot_key)
|
|
16883
|
+
REFERENCES daemon_slots(agent_name, provider, model, slot_key)
|
|
16884
|
+
ON DELETE CASCADE
|
|
16885
|
+
);
|
|
16886
|
+
|
|
16887
|
+
CREATE TABLE IF NOT EXISTS daemon_slot_workspaces (
|
|
16888
|
+
agent_name TEXT NOT NULL,
|
|
16889
|
+
provider TEXT NOT NULL,
|
|
16890
|
+
model TEXT NOT NULL,
|
|
16891
|
+
slot_key TEXT NOT NULL,
|
|
16892
|
+
workspace_id TEXT NOT NULL UNIQUE,
|
|
16893
|
+
worktree_path TEXT NOT NULL,
|
|
16894
|
+
worktree_branch TEXT,
|
|
16895
|
+
PRIMARY KEY (agent_name, provider, model, slot_key),
|
|
16896
|
+
FOREIGN KEY (agent_name, provider, model, slot_key)
|
|
16897
|
+
REFERENCES daemon_slots(agent_name, provider, model, slot_key)
|
|
16898
|
+
ON DELETE CASCADE
|
|
16899
|
+
);
|
|
16900
|
+
|
|
16901
|
+
CREATE INDEX IF NOT EXISTS daemon_slots_expires_idx
|
|
16902
|
+
ON daemon_slots (expires_at_ms);
|
|
16903
|
+
`);
|
|
16904
|
+
});
|
|
16905
|
+
} catch (error) {
|
|
16906
|
+
throw new DaemonSlotRegistryError("open database", error);
|
|
16907
|
+
}
|
|
16908
|
+
}
|
|
16909
|
+
close() {
|
|
16910
|
+
try {
|
|
16911
|
+
this.db.close();
|
|
16912
|
+
} catch (error) {
|
|
16913
|
+
throw new DaemonSlotRegistryError("close database", error);
|
|
16914
|
+
}
|
|
16915
|
+
}
|
|
16916
|
+
beginSlot(input) {
|
|
16917
|
+
const now = Date.now();
|
|
16918
|
+
const expiresAtMs = now + input.ttlSec * 1e3;
|
|
16919
|
+
this.withDb("upsert slot", () => this.db.prepare(`INSERT INTO daemon_slots (
|
|
16920
|
+
agent_name, provider, model, slot_key, task_type, state,
|
|
16921
|
+
last_task_id, last_attempt_n, created_at_ms, last_used_at_ms,
|
|
16922
|
+
expires_at_ms
|
|
16923
|
+
) VALUES (
|
|
16924
|
+
:agentName, :provider, :model, :slotKey, :taskType, 'active',
|
|
16925
|
+
:lastTaskId, :lastAttemptN, :createdAtMs, :lastUsedAtMs,
|
|
16926
|
+
:expiresAtMs
|
|
16927
|
+
)
|
|
16928
|
+
ON CONFLICT(agent_name, provider, model, slot_key) DO UPDATE SET
|
|
16929
|
+
task_type = excluded.task_type,
|
|
16930
|
+
state = 'active',
|
|
16931
|
+
last_task_id = excluded.last_task_id,
|
|
16932
|
+
last_attempt_n = excluded.last_attempt_n,
|
|
16933
|
+
last_used_at_ms = excluded.last_used_at_ms,
|
|
16934
|
+
expires_at_ms = excluded.expires_at_ms`).run({
|
|
16935
|
+
agentName: input.agentName,
|
|
16936
|
+
provider: input.provider,
|
|
16937
|
+
model: input.model,
|
|
16938
|
+
slotKey: input.slotKey,
|
|
16939
|
+
taskType: input.taskType,
|
|
16940
|
+
lastTaskId: input.lastTaskId,
|
|
16941
|
+
lastAttemptN: input.lastAttemptN,
|
|
16942
|
+
createdAtMs: now,
|
|
16943
|
+
lastUsedAtMs: now,
|
|
16944
|
+
expiresAtMs
|
|
16945
|
+
}));
|
|
16946
|
+
if (input.sessionDir) this.withDb("upsert slot session", () => this.db.prepare(`INSERT INTO daemon_slot_sessions (
|
|
16947
|
+
agent_name, provider, model, slot_key, session_dir, session_path
|
|
16948
|
+
) VALUES (
|
|
16949
|
+
:agentName, :provider, :model, :slotKey, :sessionDir, :sessionPath
|
|
16950
|
+
)
|
|
16951
|
+
ON CONFLICT(agent_name, provider, model, slot_key) DO UPDATE SET
|
|
16952
|
+
session_dir = excluded.session_dir,
|
|
16953
|
+
session_path = excluded.session_path`).run({
|
|
16954
|
+
agentName: input.agentName,
|
|
16955
|
+
provider: input.provider,
|
|
16956
|
+
model: input.model,
|
|
16957
|
+
slotKey: input.slotKey,
|
|
16958
|
+
sessionDir: input.sessionDir,
|
|
16959
|
+
sessionPath: input.sessionPath
|
|
16960
|
+
}));
|
|
16961
|
+
if (input.workspaceId && input.worktreePath) this.withDb("upsert slot workspace", () => this.db.prepare(`INSERT INTO daemon_slot_workspaces (
|
|
16962
|
+
agent_name, provider, model, slot_key, workspace_id, worktree_path,
|
|
16963
|
+
worktree_branch
|
|
16964
|
+
) VALUES (
|
|
16965
|
+
:agentName, :provider, :model, :slotKey, :workspaceId, :worktreePath,
|
|
16966
|
+
:worktreeBranch
|
|
16967
|
+
)
|
|
16968
|
+
ON CONFLICT(agent_name, provider, model, slot_key) DO UPDATE SET
|
|
16969
|
+
workspace_id = excluded.workspace_id,
|
|
16970
|
+
worktree_path = excluded.worktree_path,
|
|
16971
|
+
worktree_branch = excluded.worktree_branch`).run({
|
|
16972
|
+
agentName: input.agentName,
|
|
16973
|
+
provider: input.provider,
|
|
16974
|
+
model: input.model,
|
|
16975
|
+
slotKey: input.slotKey,
|
|
16976
|
+
workspaceId: input.workspaceId,
|
|
16977
|
+
worktreePath: input.worktreePath,
|
|
16978
|
+
worktreeBranch: input.worktreeBranch
|
|
16979
|
+
}));
|
|
16980
|
+
}
|
|
16981
|
+
finishSlot(identity, slotKey, ttlSec, sessionPath) {
|
|
16982
|
+
const now = Date.now();
|
|
16983
|
+
this.withDb("finish slot", () => this.db.prepare(`UPDATE daemon_slots
|
|
16984
|
+
SET state = 'idle',
|
|
16985
|
+
last_used_at_ms = ?,
|
|
16986
|
+
expires_at_ms = ?
|
|
16987
|
+
WHERE agent_name = ? AND provider = ? AND model = ? AND slot_key = ?`).run(now, now + ttlSec * 1e3, identity.agentName, identity.provider, identity.model, slotKey));
|
|
16988
|
+
if (sessionPath !== null) this.withDb("update slot session path", () => this.db.prepare(`UPDATE daemon_slot_sessions
|
|
16989
|
+
SET session_path = ?
|
|
16990
|
+
WHERE agent_name = ? AND provider = ? AND model = ? AND slot_key = ?`).run(sessionPath, identity.agentName, identity.provider, identity.model, slotKey));
|
|
16991
|
+
}
|
|
16992
|
+
reapExpiredSlots(now = Date.now()) {
|
|
16993
|
+
this.withDb("begin reap transaction", () => this.db.exec("BEGIN IMMEDIATE"));
|
|
16994
|
+
try {
|
|
16995
|
+
const slots = this.withDb("select expired slots", () => this.db.prepare(`SELECT
|
|
16996
|
+
agent_name as agentName,
|
|
16997
|
+
provider,
|
|
16998
|
+
model,
|
|
16999
|
+
slot_key as slotKey,
|
|
17000
|
+
task_type as taskType,
|
|
17001
|
+
state,
|
|
17002
|
+
last_task_id as lastTaskId,
|
|
17003
|
+
last_attempt_n as lastAttemptN,
|
|
17004
|
+
created_at_ms as createdAtMs,
|
|
17005
|
+
last_used_at_ms as lastUsedAtMs,
|
|
17006
|
+
expires_at_ms as expiresAtMs
|
|
17007
|
+
FROM daemon_slots
|
|
17008
|
+
WHERE expires_at_ms <= ?`).all(now));
|
|
17009
|
+
if (slots.length < 1) {
|
|
17010
|
+
this.withDb("commit empty reap transaction", () => this.db.exec("COMMIT"));
|
|
17011
|
+
return [];
|
|
17012
|
+
}
|
|
17013
|
+
const selectSession = this.withDb("prepare slot session lookup", () => this.db.prepare(`SELECT
|
|
17014
|
+
agent_name as agentName,
|
|
17015
|
+
provider,
|
|
17016
|
+
model,
|
|
17017
|
+
slot_key as slotKey,
|
|
17018
|
+
session_dir as sessionDir,
|
|
17019
|
+
session_path as sessionPath
|
|
17020
|
+
FROM daemon_slot_sessions
|
|
17021
|
+
WHERE agent_name = ? AND provider = ? AND model = ? AND slot_key = ?`));
|
|
17022
|
+
const selectWorkspace = this.withDb("prepare slot workspace lookup", () => this.db.prepare(`SELECT
|
|
17023
|
+
agent_name as agentName,
|
|
17024
|
+
provider,
|
|
17025
|
+
model,
|
|
17026
|
+
slot_key as slotKey,
|
|
17027
|
+
workspace_id as workspaceId,
|
|
17028
|
+
worktree_path as worktreePath,
|
|
17029
|
+
worktree_branch as worktreeBranch
|
|
17030
|
+
FROM daemon_slot_workspaces
|
|
17031
|
+
WHERE agent_name = ? AND provider = ? AND model = ? AND slot_key = ?`));
|
|
17032
|
+
const deleteStmt = this.withDb("prepare expired slot delete", () => this.db.prepare("DELETE FROM daemon_slots WHERE agent_name = ? AND provider = ? AND model = ? AND slot_key = ?"));
|
|
17033
|
+
const out = [];
|
|
17034
|
+
for (const slot of slots) {
|
|
17035
|
+
const session = this.withDb("select slot session", () => selectSession.get(slot.agentName, slot.provider, slot.model, slot.slotKey) ?? null);
|
|
17036
|
+
const workspace = this.withDb("select slot workspace", () => selectWorkspace.get(slot.agentName, slot.provider, slot.model, slot.slotKey) ?? null);
|
|
17037
|
+
out.push({
|
|
17038
|
+
slot,
|
|
17039
|
+
session,
|
|
17040
|
+
workspace
|
|
17041
|
+
});
|
|
17042
|
+
this.withDb("delete expired slot", () => deleteStmt.run(slot.agentName, slot.provider, slot.model, slot.slotKey));
|
|
17043
|
+
}
|
|
17044
|
+
this.withDb("commit reap transaction", () => this.db.exec("COMMIT"));
|
|
17045
|
+
for (const item of out) {
|
|
17046
|
+
if (item.session) cleanupPiSessionDir(item.session.sessionDir);
|
|
17047
|
+
if (item.workspace) cleanupReusableWorktree(item.workspace.worktreePath);
|
|
17048
|
+
}
|
|
17049
|
+
return out;
|
|
17050
|
+
} catch (error) {
|
|
17051
|
+
try {
|
|
17052
|
+
this.db.exec("ROLLBACK");
|
|
17053
|
+
} catch {}
|
|
17054
|
+
throw error instanceof DaemonSlotRegistryError ? error : new DaemonSlotRegistryError("reap expired slots", error);
|
|
17055
|
+
}
|
|
17056
|
+
}
|
|
17057
|
+
withDb(operation, fn) {
|
|
17058
|
+
try {
|
|
17059
|
+
return fn();
|
|
17060
|
+
} catch (error) {
|
|
17061
|
+
throw new DaemonSlotRegistryError(operation, error);
|
|
17062
|
+
}
|
|
17063
|
+
}
|
|
17064
|
+
};
|
|
17065
|
+
function resolveLatestPiSessionPath(sessionDir) {
|
|
17066
|
+
try {
|
|
17067
|
+
const latestEntry = readdirSync(sessionDir, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl")).map((entry) => entry.name).sort().at(-1);
|
|
17068
|
+
return latestEntry ? join(sessionDir, latestEntry) : null;
|
|
17069
|
+
} catch {
|
|
17070
|
+
return null;
|
|
17071
|
+
}
|
|
17072
|
+
}
|
|
17073
|
+
function cleanupPiSessionDir(sessionDir) {
|
|
17074
|
+
rmSync(sessionDir, {
|
|
17075
|
+
recursive: true,
|
|
17076
|
+
force: true
|
|
17077
|
+
});
|
|
17078
|
+
}
|
|
17079
|
+
function cleanupReusableWorktree(worktreePath) {
|
|
17080
|
+
const mainRepo = findMainWorktree();
|
|
17081
|
+
if (isRegisteredWorktree(mainRepo, worktreePath)) {
|
|
17082
|
+
execFileSync("git", [
|
|
17083
|
+
"-C",
|
|
17084
|
+
mainRepo,
|
|
17085
|
+
"worktree",
|
|
17086
|
+
"remove",
|
|
17087
|
+
"--force",
|
|
17088
|
+
worktreePath
|
|
17089
|
+
], { stdio: "pipe" });
|
|
17090
|
+
return;
|
|
17091
|
+
}
|
|
17092
|
+
rmSync(worktreePath, {
|
|
17093
|
+
recursive: true,
|
|
17094
|
+
force: true
|
|
17095
|
+
});
|
|
17096
|
+
}
|
|
17097
|
+
function isRegisteredWorktree(mainRepo, worktreeDir) {
|
|
17098
|
+
const list = execFileSync("git", [
|
|
17099
|
+
"-C",
|
|
17100
|
+
mainRepo,
|
|
17101
|
+
"worktree",
|
|
17102
|
+
"list",
|
|
17103
|
+
"--porcelain"
|
|
17104
|
+
], {
|
|
17105
|
+
encoding: "utf8",
|
|
17106
|
+
stdio: "pipe"
|
|
17107
|
+
});
|
|
17108
|
+
const marker = `worktree ${worktreeDir}\n`;
|
|
17109
|
+
return list.includes(marker) || list.endsWith(`worktree ${worktreeDir}`);
|
|
17110
|
+
}
|
|
17111
|
+
//#endregion
|
|
17112
|
+
//#region src/lib/slugify.ts
|
|
17113
|
+
function slugifyAsciiLower(input, maxLen, preserveChars = []) {
|
|
17114
|
+
const preserved = new Set(preserveChars.map((char) => char.toLowerCase()));
|
|
17115
|
+
let out = "";
|
|
17116
|
+
let pendingDash = false;
|
|
17117
|
+
for (const rawChar of input) {
|
|
17118
|
+
const char = rawChar.toLowerCase();
|
|
17119
|
+
const isAlphaNum = char >= "a" && char <= "z" || char >= "0" && char <= "9";
|
|
17120
|
+
const isPreserved = preserved.has(char);
|
|
17121
|
+
if (isAlphaNum || isPreserved) {
|
|
17122
|
+
if (pendingDash && out.length > 0 && out.length < maxLen) out += "-";
|
|
17123
|
+
pendingDash = false;
|
|
17124
|
+
if (out.length < maxLen) out += char;
|
|
17125
|
+
else break;
|
|
17126
|
+
continue;
|
|
17127
|
+
}
|
|
17128
|
+
pendingDash = out.length > 0;
|
|
17129
|
+
}
|
|
17130
|
+
return out;
|
|
17131
|
+
}
|
|
17132
|
+
//#endregion
|
|
17133
|
+
//#region src/lib/session-policy.ts
|
|
17134
|
+
/**
|
|
17135
|
+
* Resolve the daemon-facing execution policy for a task and, when the task
|
|
17136
|
+
* type permits warm reuse, compute the daemon-local `sessionKey`.
|
|
17137
|
+
*
|
|
17138
|
+
* `correlationId` remains the audit/query key; `sessionKey` is narrower and
|
|
17139
|
+
* may fold in task-type-specific discriminators so eval/review flows stay
|
|
17140
|
+
* isolated even inside one correlation group.
|
|
17141
|
+
*/
|
|
17142
|
+
function deriveTaskSessionDescriptor(task) {
|
|
17143
|
+
const policy = getTaskExecutionPolicy(task.taskType);
|
|
17144
|
+
return {
|
|
17145
|
+
policy,
|
|
17146
|
+
sessionKey: buildTaskSessionKey(task, policy)
|
|
17147
|
+
};
|
|
17148
|
+
}
|
|
17149
|
+
function buildTaskSessionKey(task, policy) {
|
|
17150
|
+
if (!policy.resumable) return null;
|
|
17151
|
+
switch (policy.sessionScope) {
|
|
17152
|
+
case "none": return null;
|
|
17153
|
+
case "correlation": return task.correlationId ? `${task.taskType}:correlation:${task.correlationId}` : null;
|
|
17154
|
+
case "custom": return buildCustomSessionKey(task);
|
|
17155
|
+
}
|
|
17156
|
+
}
|
|
17157
|
+
function buildCustomSessionKey(task) {
|
|
17158
|
+
switch (task.taskType) {
|
|
17159
|
+
case "run_eval": {
|
|
17160
|
+
const variantLabel = typeof task.input.variantLabel === "string" ? task.input.variantLabel : null;
|
|
17161
|
+
if (!task.correlationId || !variantLabel) return null;
|
|
17162
|
+
return `run_eval:correlation:${task.correlationId}:variant:${slugifySessionComponent(variantLabel)}`;
|
|
17163
|
+
}
|
|
17164
|
+
case "judge_eval_variant": {
|
|
17165
|
+
const runTaskIds = Array.isArray(task.input.runTaskIds) ? task.input.runTaskIds.filter((value) => typeof value === "string") : [];
|
|
17166
|
+
if (runTaskIds.length < 1) return null;
|
|
17167
|
+
return `judge_eval_variant:run_tasks:${[...runTaskIds].sort().join(",")}`;
|
|
17168
|
+
}
|
|
17169
|
+
default: return null;
|
|
17170
|
+
}
|
|
17171
|
+
}
|
|
17172
|
+
function slugifySessionComponent(input) {
|
|
17173
|
+
return slugifyAsciiLower(input.trim(), 64);
|
|
17174
|
+
}
|
|
17175
|
+
//#endregion
|
|
17176
|
+
//#region src/lib/task-execution-plan.ts
|
|
17177
|
+
function buildDaemonTaskExecutionPlan(task, stateDirs, identity, warmSessionTtlSec) {
|
|
17178
|
+
const descriptor = deriveTaskSessionDescriptor(task);
|
|
17179
|
+
const slotKey = warmSessionTtlSec > 0 ? descriptor.sessionKey : null;
|
|
17180
|
+
const workspaceScope = slotKey !== null ? descriptor.policy.workspaceScope : "attempt";
|
|
17181
|
+
const slotId = slotKey ? buildDaemonSlotId(identity, slotKey) : null;
|
|
17182
|
+
const sessionDir = slotId ? `${stateDirs.piSessionsDir}/${encodeURIComponent(slotId)}` : null;
|
|
17183
|
+
const worktreeBranch = resolveTaskWorktreeBranch(task, descriptor.policy);
|
|
17184
|
+
const workspaceId = worktreeBranch !== null ? resolveTaskWorkspaceId(task, {
|
|
17185
|
+
sessionKey: slotId,
|
|
17186
|
+
workspaceScope,
|
|
17187
|
+
sessionPersistence: sessionDir ? { sessionDir } : null
|
|
17188
|
+
}) : null;
|
|
17189
|
+
return {
|
|
17190
|
+
descriptor,
|
|
17191
|
+
sessionKey: slotId,
|
|
17192
|
+
slotKey,
|
|
17193
|
+
slotId,
|
|
17194
|
+
workspaceScope,
|
|
17195
|
+
sessionPersistence: sessionDir ? { sessionDir } : null,
|
|
17196
|
+
workspaceId,
|
|
17197
|
+
worktreeBranch
|
|
17198
|
+
};
|
|
17199
|
+
}
|
|
17200
|
+
function buildDaemonSlotId(identity, slotKey) {
|
|
17201
|
+
return [
|
|
17202
|
+
"agent",
|
|
17203
|
+
slugSlotIdentityComponent(identity.agentName),
|
|
17204
|
+
"provider",
|
|
17205
|
+
slugSlotIdentityComponent(identity.provider),
|
|
17206
|
+
"model",
|
|
17207
|
+
slugSlotIdentityComponent(identity.model),
|
|
17208
|
+
"key",
|
|
17209
|
+
slotKey
|
|
17210
|
+
].join(":");
|
|
17211
|
+
}
|
|
17212
|
+
function slugSlotIdentityComponent(input) {
|
|
17213
|
+
return slugifyAsciiLower(input.trim(), 64, [
|
|
17214
|
+
".",
|
|
17215
|
+
"_",
|
|
17216
|
+
"-"
|
|
17217
|
+
]);
|
|
17218
|
+
}
|
|
17219
|
+
function resolveTaskWorktreeBranch(task, policy) {
|
|
17220
|
+
if (policy.workspaceMode !== "dedicated_worktree") return null;
|
|
17221
|
+
if (task.taskType === "fulfill_brief") {
|
|
17222
|
+
const input = task.input;
|
|
17223
|
+
const slug = slugifyAsciiLower(typeof input.title === "string" && input.title.trim().length > 0 ? input.title : typeof input.brief === "string" && input.brief.trim().length > 0 ? input.brief : task.taskType, 60) || "task";
|
|
17224
|
+
if (task.correlationId) return `moltnet/${task.correlationId}/${slug}`;
|
|
17225
|
+
return `feat/${(typeof input.scopeHint === "string" && input.scopeHint.trim().length > 0 ? slugifyAsciiLower(input.scopeHint, 60) : "task") || "task"}-${slug}`;
|
|
17226
|
+
}
|
|
17227
|
+
return `task/${slugifyAsciiLower(task.taskType, 60) || "task"}-${task.id.slice(0, 8)}`;
|
|
17228
|
+
}
|
|
17229
|
+
function resolveTaskWorkspaceId(task, executionPlan) {
|
|
17230
|
+
if (executionPlan.workspaceScope === "session" && executionPlan.sessionKey !== null) return `session-${encodeURIComponent(executionPlan.sessionKey)}`;
|
|
17231
|
+
return `task-${task.id}`;
|
|
17232
|
+
}
|
|
17233
|
+
//#endregion
|
|
17234
|
+
//#region src/lib/execution-plan-cache.ts
|
|
17235
|
+
function createExecutionPlanCache(args) {
|
|
17236
|
+
const cache = /* @__PURE__ */ new Map();
|
|
17237
|
+
return {
|
|
17238
|
+
getOrCreate(claimedTask) {
|
|
17239
|
+
const key = buildClaimedTaskKey(claimedTask);
|
|
17240
|
+
const existing = cache.get(key);
|
|
17241
|
+
if (existing) return existing;
|
|
17242
|
+
const plan = buildDaemonTaskExecutionPlan(claimedTask.task, args.stateDirs, args.slotIdentity, args.warmSessionTtlSec);
|
|
17243
|
+
cache.set(key, plan);
|
|
17244
|
+
return plan;
|
|
17245
|
+
},
|
|
17246
|
+
delete(claimedTask) {
|
|
17247
|
+
cache.delete(buildClaimedTaskKey(claimedTask));
|
|
17248
|
+
}
|
|
17249
|
+
};
|
|
17250
|
+
}
|
|
17251
|
+
function buildClaimedTaskKey(task) {
|
|
17252
|
+
return `${task.task.id}:${task.attemptN}`;
|
|
17253
|
+
}
|
|
17254
|
+
//#endregion
|
|
16629
17255
|
//#region src/lib/finalize.ts
|
|
16630
17256
|
async function finalizeTask(agent, output, ctx = {}) {
|
|
16631
17257
|
if (output.status === "cancelled") return;
|
|
@@ -16710,7 +17336,8 @@ var DEFAULTS = {
|
|
|
16710
17336
|
maxBatchSize: 50,
|
|
16711
17337
|
flushIntervalMs: 200,
|
|
16712
17338
|
maxTurns: 0,
|
|
16713
|
-
maxBashTimeouts: 3
|
|
17339
|
+
maxBashTimeouts: 3,
|
|
17340
|
+
warmSessionTtlSec: 1800
|
|
16714
17341
|
};
|
|
16715
17342
|
var MissingRequiredOptionError = class extends Error {
|
|
16716
17343
|
constructor(flag) {
|
|
@@ -16734,6 +17361,7 @@ function parseCommonOptions(args) {
|
|
|
16734
17361
|
flushIntervalMs: parseNonNegativeInt(args["flush-interval-ms"], "flush-interval-ms", DEFAULTS.flushIntervalMs),
|
|
16735
17362
|
maxTurns: parseNonNegativeInt(args["max-turns"], "max-turns", DEFAULTS.maxTurns),
|
|
16736
17363
|
maxBashTimeouts: parseNonNegativeInt(args["max-bash-timeouts"], "max-bash-timeouts", DEFAULTS.maxBashTimeouts),
|
|
17364
|
+
warmSessionTtlSec: parseNonNegativeInt(args["warm-session-ttl-sec"], "warm-session-ttl-sec", DEFAULTS.warmSessionTtlSec),
|
|
16737
17365
|
debug: args.debug === true
|
|
16738
17366
|
};
|
|
16739
17367
|
}
|
|
@@ -16769,6 +17397,7 @@ function commonOptionDefs() {
|
|
|
16769
17397
|
"flush-interval-ms": { type: "string" },
|
|
16770
17398
|
"max-turns": { type: "string" },
|
|
16771
17399
|
"max-bash-timeouts": { type: "string" },
|
|
17400
|
+
"warm-session-ttl-sec": { type: "string" },
|
|
16772
17401
|
debug: { type: "boolean" }
|
|
16773
17402
|
};
|
|
16774
17403
|
}
|
|
@@ -16851,6 +17480,18 @@ function findUp(startDir, filename) {
|
|
|
16851
17480
|
}
|
|
16852
17481
|
}
|
|
16853
17482
|
//#endregion
|
|
17483
|
+
//#region src/lib/state-dir.ts
|
|
17484
|
+
function ensureDaemonStateDirs(mountPath) {
|
|
17485
|
+
const rootDir = join(mountPath, ".moltnet", "d");
|
|
17486
|
+
const piSessionsDir = join(rootDir, "pi-sessions");
|
|
17487
|
+
mkdirSync(piSessionsDir, { recursive: true });
|
|
17488
|
+
return {
|
|
17489
|
+
rootDir,
|
|
17490
|
+
piSessionsDir,
|
|
17491
|
+
registryDbPath: join(rootDir, "daemon-state.sqlite")
|
|
17492
|
+
};
|
|
17493
|
+
}
|
|
17494
|
+
//#endregion
|
|
16854
17495
|
//#region src/lib/turn-event-logger.ts
|
|
16855
17496
|
function makeTurnEventHandler(base, context = {}) {
|
|
16856
17497
|
const log = base.child({
|
|
@@ -16922,6 +17563,19 @@ async function runPolling(opts) {
|
|
|
16922
17563
|
const listLimit = optionalPositiveInt(values["list-limit"], "list-limit", 10);
|
|
16923
17564
|
if (taskTypes.length === 0) console.error(`[${opts.modeLabel}] --task-types is empty — daemon will accept any registered type. Pass an explicit list to limit scope (e.g. --task-types fulfill_brief).`);
|
|
16924
17565
|
const sandbox = resolveSandbox(process.cwd(), values.sandbox);
|
|
17566
|
+
const stateDirs = ensureDaemonStateDirs(sandbox.rootDir);
|
|
17567
|
+
const slotRegistry = new DaemonSlotRegistry(stateDirs.registryDbPath);
|
|
17568
|
+
const slotIdentity = {
|
|
17569
|
+
agentName: common.agent,
|
|
17570
|
+
provider: common.provider,
|
|
17571
|
+
model: common.model
|
|
17572
|
+
};
|
|
17573
|
+
const mainRepo = findMainWorktree();
|
|
17574
|
+
const executionPlans = createExecutionPlanCache({
|
|
17575
|
+
stateDirs,
|
|
17576
|
+
slotIdentity,
|
|
17577
|
+
warmSessionTtlSec: common.warmSessionTtlSec
|
|
17578
|
+
});
|
|
16925
17579
|
const ctx = await resolveAgentContext(common.agent);
|
|
16926
17580
|
const cfg = loadConfig();
|
|
16927
17581
|
const otelShutdown = await initWorkerOtel({
|
|
@@ -16972,6 +17626,7 @@ async function runPolling(opts) {
|
|
|
16972
17626
|
provider: common.provider,
|
|
16973
17627
|
model: common.model,
|
|
16974
17628
|
sandboxConfig: sandbox.config,
|
|
17629
|
+
makeExecutionPlan: (claimedTask) => executionPlans.getOrCreate(claimedTask),
|
|
16975
17630
|
makeOnTurnEvent: makeTurnEventHandlerFactory(rootLogger),
|
|
16976
17631
|
maxTurns: common.maxTurns,
|
|
16977
17632
|
maxBashTimeouts: common.maxBashTimeouts
|
|
@@ -17010,6 +17665,26 @@ async function runPolling(opts) {
|
|
|
17010
17665
|
log: (msg, err) => rootLogger.warn({ err }, msg)
|
|
17011
17666
|
}),
|
|
17012
17667
|
executeTask: async (claimedTask, reporter) => {
|
|
17668
|
+
const executionPlan = executionPlans.getOrCreate(claimedTask);
|
|
17669
|
+
const sessionDescriptor = executionPlan.descriptor;
|
|
17670
|
+
const expired = slotRegistry.reapExpiredSlots();
|
|
17671
|
+
if (expired.length > 0) rootLogger.info({
|
|
17672
|
+
expiredCount: expired.length,
|
|
17673
|
+
slotKeys: expired.map((item) => item.slot.slotKey)
|
|
17674
|
+
}, "agent-daemon.daemon_slots_reaped");
|
|
17675
|
+
rootLogger.debug({
|
|
17676
|
+
taskId: claimedTask.task.id,
|
|
17677
|
+
taskType: claimedTask.task.taskType,
|
|
17678
|
+
resumable: sessionDescriptor.policy.resumable,
|
|
17679
|
+
workspaceMode: sessionDescriptor.policy.workspaceMode,
|
|
17680
|
+
workspaceScope: sessionDescriptor.policy.workspaceScope,
|
|
17681
|
+
sessionScope: sessionDescriptor.policy.sessionScope,
|
|
17682
|
+
slotKey: executionPlan.slotKey,
|
|
17683
|
+
slotId: executionPlan.slotId,
|
|
17684
|
+
sessionKey: executionPlan.sessionKey,
|
|
17685
|
+
piSessionDir: executionPlan.sessionPersistence?.sessionDir ?? null,
|
|
17686
|
+
workspaceId: executionPlan.workspaceId
|
|
17687
|
+
}, "agent-daemon.task_execution_policy");
|
|
17013
17688
|
if (taskTypes.length > 0 && !taskTypes.includes(claimedTask.task.taskType)) return {
|
|
17014
17689
|
taskId: claimedTask.task.id,
|
|
17015
17690
|
attemptN: claimedTask.attemptN,
|
|
@@ -17044,7 +17719,25 @@ async function runPolling(opts) {
|
|
|
17044
17719
|
retryable: false
|
|
17045
17720
|
}
|
|
17046
17721
|
};
|
|
17047
|
-
|
|
17722
|
+
if (executionPlan.slotKey && executionPlan.sessionPersistence) slotRegistry.beginSlot({
|
|
17723
|
+
...slotIdentity,
|
|
17724
|
+
slotKey: executionPlan.slotKey,
|
|
17725
|
+
taskType: claimedTask.task.taskType,
|
|
17726
|
+
sessionDir: executionPlan.sessionPersistence.sessionDir,
|
|
17727
|
+
sessionPath: resolveLatestPiSessionPath(executionPlan.sessionPersistence.sessionDir),
|
|
17728
|
+
workspaceId: executionPlan.workspaceId,
|
|
17729
|
+
worktreePath: executionPlan.workspaceId ? resolveTaskWorktreePath(mainRepo, executionPlan.workspaceId) : null,
|
|
17730
|
+
worktreeBranch: executionPlan.worktreeBranch,
|
|
17731
|
+
lastTaskId: claimedTask.task.id,
|
|
17732
|
+
lastAttemptN: claimedTask.attemptN,
|
|
17733
|
+
ttlSec: common.warmSessionTtlSec
|
|
17734
|
+
});
|
|
17735
|
+
try {
|
|
17736
|
+
return await executeTask(claimedTask, reporter);
|
|
17737
|
+
} finally {
|
|
17738
|
+
executionPlans.delete(claimedTask);
|
|
17739
|
+
if (executionPlan.slotKey) slotRegistry.finishSlot(slotIdentity, executionPlan.slotKey, common.warmSessionTtlSec, executionPlan.sessionPersistence ? resolveLatestPiSessionPath(executionPlan.sessionPersistence.sessionDir) : null);
|
|
17740
|
+
}
|
|
17048
17741
|
}
|
|
17049
17742
|
});
|
|
17050
17743
|
const drained = await runtime.start();
|
|
@@ -17052,6 +17745,7 @@ async function runPolling(opts) {
|
|
|
17052
17745
|
rootLogger.info({ processed: drained.length }, "agent-daemon.drained");
|
|
17053
17746
|
return drained.some((o) => o.status !== "completed") ? 1 : 0;
|
|
17054
17747
|
} finally {
|
|
17748
|
+
slotRegistry.close();
|
|
17055
17749
|
await otelShutdown();
|
|
17056
17750
|
await shutdownLogger();
|
|
17057
17751
|
}
|
|
@@ -17112,6 +17806,19 @@ async function runOnce(argv) {
|
|
|
17112
17806
|
throw err;
|
|
17113
17807
|
}
|
|
17114
17808
|
const sandbox = resolveSandbox(process.cwd(), values.sandbox);
|
|
17809
|
+
const stateDirs = ensureDaemonStateDirs(sandbox.rootDir);
|
|
17810
|
+
const slotRegistry = new DaemonSlotRegistry(stateDirs.registryDbPath);
|
|
17811
|
+
const slotIdentity = {
|
|
17812
|
+
agentName: opts.agent,
|
|
17813
|
+
provider: opts.provider,
|
|
17814
|
+
model: opts.model
|
|
17815
|
+
};
|
|
17816
|
+
const mainRepo = findMainWorktree();
|
|
17817
|
+
const executionPlans = createExecutionPlanCache({
|
|
17818
|
+
stateDirs,
|
|
17819
|
+
slotIdentity,
|
|
17820
|
+
warmSessionTtlSec: opts.warmSessionTtlSec
|
|
17821
|
+
});
|
|
17115
17822
|
const ctx = await resolveAgentContext(opts.agent);
|
|
17116
17823
|
const cfg = loadConfig();
|
|
17117
17824
|
const otelShutdown = await initWorkerOtel({
|
|
@@ -17160,16 +17867,44 @@ async function runOnce(argv) {
|
|
|
17160
17867
|
onSignal("SIGTERM");
|
|
17161
17868
|
});
|
|
17162
17869
|
try {
|
|
17163
|
-
const
|
|
17870
|
+
const rawExecuteTask = createPiTaskExecutor({
|
|
17164
17871
|
agentName: opts.agent,
|
|
17165
17872
|
mountPath: sandbox.rootDir,
|
|
17166
17873
|
provider: opts.provider,
|
|
17167
17874
|
model: opts.model,
|
|
17168
17875
|
sandboxConfig: sandbox.config,
|
|
17876
|
+
makeExecutionPlan: (claimedTask) => executionPlans.getOrCreate(claimedTask),
|
|
17169
17877
|
onTurnEvent: makeTurnEventHandler(rootLogger, { taskId }),
|
|
17170
17878
|
maxTurns: opts.maxTurns,
|
|
17171
17879
|
maxBashTimeouts: opts.maxBashTimeouts
|
|
17172
17880
|
});
|
|
17881
|
+
const executeTask = async (claimedTask, reporter) => {
|
|
17882
|
+
const expired = slotRegistry.reapExpiredSlots();
|
|
17883
|
+
if (expired.length > 0) rootLogger.info({
|
|
17884
|
+
expiredCount: expired.length,
|
|
17885
|
+
slotKeys: expired.map((item) => item.slot.slotKey)
|
|
17886
|
+
}, "agent-daemon.daemon_slots_reaped");
|
|
17887
|
+
const executionPlan = executionPlans.getOrCreate(claimedTask);
|
|
17888
|
+
if (executionPlan.slotKey && executionPlan.sessionPersistence) slotRegistry.beginSlot({
|
|
17889
|
+
...slotIdentity,
|
|
17890
|
+
slotKey: executionPlan.slotKey,
|
|
17891
|
+
taskType: claimedTask.task.taskType,
|
|
17892
|
+
sessionDir: executionPlan.sessionPersistence.sessionDir,
|
|
17893
|
+
sessionPath: resolveLatestPiSessionPath(executionPlan.sessionPersistence.sessionDir),
|
|
17894
|
+
workspaceId: executionPlan.workspaceId,
|
|
17895
|
+
worktreePath: executionPlan.workspaceId ? resolveTaskWorktreePath(mainRepo, executionPlan.workspaceId) : null,
|
|
17896
|
+
worktreeBranch: executionPlan.worktreeBranch,
|
|
17897
|
+
lastTaskId: claimedTask.task.id,
|
|
17898
|
+
lastAttemptN: claimedTask.attemptN,
|
|
17899
|
+
ttlSec: opts.warmSessionTtlSec
|
|
17900
|
+
});
|
|
17901
|
+
try {
|
|
17902
|
+
return await rawExecuteTask(claimedTask, reporter);
|
|
17903
|
+
} finally {
|
|
17904
|
+
executionPlans.delete(claimedTask);
|
|
17905
|
+
if (executionPlan.slotKey) slotRegistry.finishSlot(slotIdentity, executionPlan.slotKey, opts.warmSessionTtlSec, executionPlan.sessionPersistence ? resolveLatestPiSessionPath(executionPlan.sessionPersistence.sessionDir) : null);
|
|
17906
|
+
}
|
|
17907
|
+
};
|
|
17173
17908
|
const writeCorrelationAnchors = makePrBodyAnchorWriter({
|
|
17174
17909
|
gh: createGhCliClient(),
|
|
17175
17910
|
logger: rootLogger
|
|
@@ -17204,6 +17939,7 @@ async function runOnce(argv) {
|
|
|
17204
17939
|
console.log(JSON.stringify(output, null, 2));
|
|
17205
17940
|
return output.status === "completed" ? 0 : 1;
|
|
17206
17941
|
} finally {
|
|
17942
|
+
slotRegistry.close();
|
|
17207
17943
|
await otelShutdown();
|
|
17208
17944
|
await shutdownLogger();
|
|
17209
17945
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@themoltnet/agent-daemon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "MoltNet agent daemon — claims and executes tasks (fulfill_brief, assess_brief) from the MoltNet task-service via Pi-headless. CLI: moltnet-agent.",
|
|
@@ -33,19 +33,19 @@
|
|
|
33
33
|
"@opentelemetry/semantic-conventions": "^1.39.0",
|
|
34
34
|
"pino": "^10.3.1",
|
|
35
35
|
"pino-pretty": "^13.1.3",
|
|
36
|
-
"@themoltnet/
|
|
37
|
-
"@themoltnet/
|
|
38
|
-
"@themoltnet/
|
|
36
|
+
"@themoltnet/pi-extension": "0.17.0",
|
|
37
|
+
"@themoltnet/agent-runtime": "0.15.1",
|
|
38
|
+
"@themoltnet/sdk": "0.102.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"tsx": "^4.7.0",
|
|
42
42
|
"typescript": "^5.3.3",
|
|
43
43
|
"vite": "^8.0.0",
|
|
44
44
|
"vitest": "^3.0.0",
|
|
45
|
-
"@moltnet/
|
|
46
|
-
"@moltnet/tasks": "0.1.0",
|
|
45
|
+
"@moltnet/bootstrap": "0.1.0",
|
|
47
46
|
"@moltnet/database": "0.1.0",
|
|
48
|
-
"@moltnet/
|
|
47
|
+
"@moltnet/tasks": "0.1.0",
|
|
48
|
+
"@moltnet/crypto-service": "0.1.0"
|
|
49
49
|
},
|
|
50
50
|
"nx": {
|
|
51
51
|
"tags": [
|