agent-dealer 0.2.0 → 1.0.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/bundle/server/dist/adapters/agent-deck-bind.js +443 -0
- package/bundle/server/dist/adapters/agent-deck-bind.test.js +454 -0
- package/bundle/server/dist/adapters/agent-deck.js +38 -50
- package/bundle/server/dist/adapters/agent-deck.test.js +63 -0
- package/bundle/server/dist/adapters/agent-health.js +230 -33
- package/bundle/server/dist/adapters/agent-health.test.js +237 -0
- package/bundle/server/dist/adapters/authority-lifecycle.js +289 -0
- package/bundle/server/dist/adapters/codex-scoped-config.js +103 -0
- package/bundle/server/dist/adapters/execution-authority.js +103 -0
- package/bundle/server/dist/adapters/execution-authority.test.js +166 -0
- package/bundle/server/dist/adapters/external.js +0 -1
- package/bundle/server/dist/adapters/git-worktree.js +514 -0
- package/bundle/server/dist/adapters/git-worktree.test.js +502 -0
- package/bundle/server/dist/adapters/github.js +170 -0
- package/bundle/server/dist/adapters/github.test.js +205 -0
- package/bundle/server/dist/adapters/linear-inbox.js +199 -30
- package/bundle/server/dist/adapters/linear-inbox.test.js +35 -0
- package/bundle/server/dist/adapters/linear-sync.js +3 -101
- package/bundle/server/dist/adapters/managed-repo.js +168 -0
- package/bundle/server/dist/adapters/managed-repo.test.js +63 -0
- package/bundle/server/dist/adapters/outbound-delivery.js +83 -0
- package/bundle/server/dist/adapters/outbound-delivery.test.js +53 -0
- package/bundle/server/dist/adapters/reflect-authority.js +43 -0
- package/bundle/server/dist/cli-env.js +26 -0
- package/bundle/server/dist/cli-env.test.js +14 -1
- package/bundle/server/dist/coordinator/abort.integration.test.js +74 -0
- package/bundle/server/dist/coordinator/admission.js +330 -0
- package/bundle/server/dist/coordinator/admission.test.js +338 -0
- package/bundle/server/dist/coordinator/args.js +128 -0
- package/bundle/server/dist/coordinator/args.test.js +132 -0
- package/bundle/server/dist/coordinator/authority-recovery.integration.test.js +661 -0
- package/bundle/server/dist/coordinator/auto-merge.integration.test.js +363 -0
- package/bundle/server/dist/coordinator/auto-merge.js +286 -0
- package/bundle/server/dist/coordinator/auto-merge.timeout.test.js +14 -0
- package/bundle/server/dist/coordinator/branch-progress.js +81 -0
- package/bundle/server/dist/coordinator/clock-jump.js +98 -0
- package/bundle/server/dist/coordinator/commands.js +1088 -0
- package/bundle/server/dist/coordinator/commands.test.js +554 -0
- package/bundle/server/dist/coordinator/deck-outage-config.js +32 -0
- package/bundle/server/dist/coordinator/deck-outage.integration.test.js +200 -0
- package/bundle/server/dist/coordinator/dependencies.js +242 -0
- package/bundle/server/dist/coordinator/dependency-readiness.integration.test.js +535 -0
- package/bundle/server/dist/coordinator/developer-effect.js +959 -0
- package/bundle/server/dist/coordinator/developer-effect.test.js +1143 -0
- package/bundle/server/dist/coordinator/dispatcher.js +58 -0
- package/bundle/server/dist/coordinator/dispatcher.test.js +81 -0
- package/bundle/server/dist/coordinator/effect-registry.js +17 -0
- package/bundle/server/dist/coordinator/execution-env.integration.test.js +122 -0
- package/bundle/server/dist/coordinator/failure-reason.js +273 -0
- package/bundle/server/dist/coordinator/failure-reason.test.js +234 -0
- package/bundle/server/dist/coordinator/guidance.js +33 -0
- package/bundle/server/dist/coordinator/guidance.test.js +110 -0
- package/bundle/server/dist/coordinator/human-resolution.js +88 -0
- package/bundle/server/dist/coordinator/human-resolution.test.js +72 -0
- package/bundle/server/dist/coordinator/intent-forecast.js +13 -0
- package/bundle/server/dist/coordinator/intent-forecast.test.js +34 -0
- package/bundle/server/dist/coordinator/latest-failure.js +100 -0
- package/bundle/server/dist/coordinator/latest-failure.test.js +125 -0
- package/bundle/server/dist/coordinator/metrics.js +35 -0
- package/bundle/server/dist/coordinator/metrics.test.js +52 -0
- package/bundle/server/dist/coordinator/permissions.js +127 -0
- package/bundle/server/dist/coordinator/permissions.test.js +184 -0
- package/bundle/server/dist/coordinator/presumed-dead-budget.integration.test.js +146 -0
- package/bundle/server/dist/coordinator/process-liveness.js +191 -0
- package/bundle/server/dist/coordinator/profile-snapshot.js +20 -0
- package/bundle/server/dist/coordinator/profile-snapshot.test.js +118 -0
- package/bundle/server/dist/coordinator/projection.js +147 -0
- package/bundle/server/dist/coordinator/projection.test.js +121 -0
- package/bundle/server/dist/coordinator/prompts.js +164 -0
- package/bundle/server/dist/coordinator/prompts.test.js +177 -0
- package/bundle/server/dist/coordinator/queue-by-default.integration.test.js +271 -0
- package/bundle/server/dist/coordinator/reclaim-republish.integration.test.js +372 -0
- package/bundle/server/dist/coordinator/recovery.js +466 -0
- package/bundle/server/dist/coordinator/recovery.test.js +164 -0
- package/bundle/server/dist/coordinator/reflect-trigger.js +218 -0
- package/bundle/server/dist/coordinator/reflect-trigger.test.js +324 -0
- package/bundle/server/dist/coordinator/repair-cycle.integration.test.js +234 -0
- package/bundle/server/dist/coordinator/restart-recovery.integration.test.js +363 -0
- package/bundle/server/dist/coordinator/reviewer-effect.js +507 -0
- package/bundle/server/dist/coordinator/reviewer-effect.test.js +612 -0
- package/bundle/server/dist/coordinator/reviewer-result.js +38 -0
- package/bundle/server/dist/coordinator/reviewer-result.test.js +45 -0
- package/bundle/server/dist/coordinator/routing.js +160 -0
- package/bundle/server/dist/coordinator/routing.test.js +336 -0
- package/bundle/server/dist/coordinator/session-lifecycle.integration.test.js +127 -0
- package/bundle/server/dist/coordinator/session-lifecycle.js +280 -0
- package/bundle/server/dist/coordinator/session-lifecycle.test.js +121 -0
- package/bundle/server/dist/coordinator/session-progress.js +351 -0
- package/bundle/server/dist/coordinator/session-progress.test.js +190 -0
- package/bundle/server/dist/coordinator/session-timeouts.js +33 -0
- package/bundle/server/dist/coordinator/sleep-recovery.integration.test.js +233 -0
- package/bundle/server/dist/coordinator/spawn.js +49 -0
- package/bundle/server/dist/coordinator/sync-issue-base-branch.js +18 -0
- package/bundle/server/dist/coordinator/usage-cap-config.js +10 -0
- package/bundle/server/dist/coordinator/usage-cap-defer.js +189 -0
- package/bundle/server/dist/coordinator/usage-cap.integration.test.js +193 -0
- package/bundle/server/dist/coordinator/usage.js +40 -0
- package/bundle/server/dist/coordinator/usage.test.js +69 -0
- package/bundle/server/dist/coordinator/verification-receipt.js +333 -0
- package/bundle/server/dist/coordinator/verification-receipt.test.js +382 -0
- package/bundle/server/dist/coordinator/worker-loop.js +363 -0
- package/bundle/server/dist/coordinator/worker-loop.test.js +327 -0
- package/bundle/server/dist/coordinator/workflow-instances.test.js +45 -0
- package/bundle/server/dist/coordinator/worktree-owner-liveness.js +82 -0
- package/bundle/server/dist/coordinator/worktree-owner-liveness.test.js +199 -0
- package/bundle/server/dist/db/artifacts-issue-columns.test.js +18 -0
- package/bundle/server/dist/db/authority-attempts-stale-at-migration.test.js +65 -0
- package/bundle/server/dist/db/idempotency-index-migration.test.js +57 -0
- package/bundle/server/dist/db/index.js +359 -7
- package/bundle/server/dist/db/linear-inbox-unlock.test.js +67 -0
- package/bundle/server/dist/db/migrate-to-issues.js +824 -0
- package/bundle/server/dist/db/migrate-to-issues.test.js +842 -0
- package/bundle/server/dist/db/schema.sql +311 -1
- package/bundle/server/dist/db/schema.test.js +66 -0
- package/bundle/server/dist/db/worker-session-pid-migration.test.js +112 -0
- package/bundle/server/dist/dev-review-cli-happy-path.integration.test.js +338 -0
- package/bundle/server/dist/direct-start-liveness.integration.test.js +350 -0
- package/bundle/server/dist/index.js +58 -5
- package/bundle/server/dist/paths.js +40 -4
- package/bundle/server/dist/paths.test.js +42 -0
- package/bundle/server/dist/pid-marker-claim-child.js +18 -0
- package/bundle/server/dist/pid-marker-hold-arbiter-child.js +24 -0
- package/bundle/server/dist/pid-marker.js +141 -0
- package/bundle/server/dist/pid-marker.test.js +208 -0
- package/bundle/server/dist/queue/approve-deliver.js +128 -28
- package/bundle/server/dist/queue/reflect-schedule.js +25 -0
- package/bundle/server/dist/queue/result-qa.js +1 -1
- package/bundle/server/dist/queue/send-gate.test.js +172 -36
- package/bundle/server/dist/repository/agents.js +55 -18
- package/bundle/server/dist/repository/artifacts-for-issue.js +42 -0
- package/bundle/server/dist/repository/artifacts-for-issue.test.js +48 -0
- package/bundle/server/dist/repository/artifacts.js +47 -0
- package/bundle/server/dist/repository/artifacts.test.js +48 -0
- package/bundle/server/dist/repository/authority-attempts.js +189 -0
- package/bundle/server/dist/repository/findings.js +70 -0
- package/bundle/server/dist/repository/findings.test.js +68 -0
- package/bundle/server/dist/repository/human-actions.js +142 -0
- package/bundle/server/dist/repository/human-actions.test.js +123 -0
- package/bundle/server/dist/repository/intake-settings.js +5 -59
- package/bundle/server/dist/repository/issues.js +305 -0
- package/bundle/server/dist/repository/issues.test.js +69 -0
- package/bundle/server/dist/repository/outbound-drafts.js +28 -0
- package/bundle/server/dist/repository/queue-entries.js +162 -0
- package/bundle/server/dist/repository/review-publications.js +153 -0
- package/bundle/server/dist/repository/review-publications.test.js +78 -0
- package/bundle/server/dist/repository/runs-external-id.test.js +5 -3
- package/bundle/server/dist/repository/runs.js +4 -211
- package/bundle/server/dist/repository/runtime-availability.js +54 -0
- package/bundle/server/dist/repository/usage-events.js +60 -0
- package/bundle/server/dist/repository/usage-events.test.js +56 -0
- package/bundle/server/dist/repository/work-items.js +305 -0
- package/bundle/server/dist/repository/work-items.test.js +142 -0
- package/bundle/server/dist/repository/worker-sessions.js +219 -0
- package/bundle/server/dist/repository/worker-sessions.test.js +60 -0
- package/bundle/server/dist/repository/workflow-events.js +193 -0
- package/bundle/server/dist/repository/workflow-events.test.js +89 -0
- package/bundle/server/dist/resolve-tsx-bin.js +19 -0
- package/bundle/server/dist/routes/human-actions.js +68 -0
- package/bundle/server/dist/routes/human-actions.test.js +284 -0
- package/bundle/server/dist/routes/index.js +29 -496
- package/bundle/server/dist/routes/issues.js +240 -0
- package/bundle/server/dist/routes/issues.test.js +451 -0
- package/bundle/server/dist/routes/queue.js +41 -0
- package/bundle/server/dist/routes/queue.test.js +78 -0
- package/bundle/server/dist/runners/claude-args.js +18 -30
- package/bundle/server/dist/runners/claude.js +19 -163
- package/bundle/server/dist/runners/claude.test.js +15 -70
- package/bundle/server/dist/runners/codex-args.js +59 -0
- package/bundle/server/dist/runners/codex-args.test.js +72 -0
- package/bundle/server/dist/runners/codex-jsonl.js +150 -0
- package/bundle/server/dist/runners/codex-jsonl.test.js +36 -0
- package/bundle/server/dist/runners/codex-resume.test.js +19 -0
- package/bundle/server/dist/runners/codex.js +46 -0
- package/bundle/server/dist/runners/models-codex.test.js +14 -0
- package/bundle/server/dist/runners/models.js +53 -1
- package/bundle/server/dist/runners/persist.js +4 -1
- package/bundle/server/dist/runners/process-registry.js +11 -0
- package/bundle/server/dist/runners/prompts.js +13 -280
- package/bundle/server/dist/runners/prompts.test.js +22 -153
- package/bundle/server/dist/runners/qa.js +17 -1
- package/bundle/server/dist/runners/reflect.js +4 -16
- package/bundle/server/dist/runners/spawn-cli.js +197 -0
- package/bundle/server/dist/runners/spawn-cli.test.js +130 -0
- package/bundle/server/dist/runners/stream-json.js +0 -141
- package/bundle/server/dist/runners/usage-cap.js +242 -0
- package/bundle/server/dist/runners/usage-cap.test.js +97 -0
- package/bundle/server/dist/server-liveness.js +37 -0
- package/bundle/server/dist/server-liveness.test.js +82 -0
- package/bundle/server/dist/static-ui.test.js +44 -0
- package/bundle/server/package.json +4 -2
- package/bundle/server/static-ui/assets/index-BTtPGYpY.js +60 -0
- package/bundle/server/static-ui/assets/index-BxTx4b1d.css +1 -0
- package/bundle/server/static-ui/index.html +2 -2
- package/bundle/shared/dist/agents.d.ts +262 -133
- package/bundle/shared/dist/agents.js +85 -19
- package/bundle/shared/dist/cursor-auth-health.d.ts +12 -0
- package/bundle/shared/dist/cursor-auth-health.js +36 -0
- package/bundle/shared/dist/cursor-auth-health.test.d.ts +1 -0
- package/bundle/shared/dist/cursor-auth-health.test.js +40 -0
- package/bundle/shared/dist/findings.d.ts +46 -0
- package/bundle/shared/dist/findings.js +17 -0
- package/bundle/shared/dist/findings.test.d.ts +1 -0
- package/bundle/shared/dist/findings.test.js +20 -0
- package/bundle/shared/dist/github-repo.d.ts +28 -0
- package/bundle/shared/dist/github-repo.js +98 -0
- package/bundle/shared/dist/github-repo.test.d.ts +1 -0
- package/bundle/shared/dist/github-repo.test.js +32 -0
- package/bundle/shared/dist/human-actions.d.ts +66 -0
- package/bundle/shared/dist/human-actions.js +51 -0
- package/bundle/shared/dist/human-actions.test.d.ts +1 -0
- package/bundle/shared/dist/human-actions.test.js +57 -0
- package/bundle/shared/dist/index.d.ts +567 -757
- package/bundle/shared/dist/index.js +22 -36
- package/bundle/shared/dist/issues.d.ts +385 -0
- package/bundle/shared/dist/issues.js +169 -0
- package/bundle/shared/dist/issues.test.d.ts +1 -0
- package/bundle/shared/dist/issues.test.js +72 -0
- package/bundle/shared/dist/outbound-draft.d.ts +6 -0
- package/bundle/shared/dist/outbound-draft.js +4 -0
- package/bundle/shared/dist/plan-triage.d.ts +6 -6
- package/bundle/shared/dist/profile-snapshot.d.ts +154 -0
- package/bundle/shared/dist/profile-snapshot.js +145 -0
- package/bundle/shared/dist/profile-snapshot.test.d.ts +1 -0
- package/bundle/shared/dist/profile-snapshot.test.js +67 -0
- package/bundle/shared/dist/queue-entries.d.ts +89 -0
- package/bundle/shared/dist/queue-entries.js +19 -0
- package/bundle/shared/dist/result-qa.d.ts +2 -2
- package/bundle/shared/dist/runtime-auth-health.d.ts +66 -0
- package/bundle/shared/dist/runtime-auth-health.js +186 -0
- package/bundle/shared/dist/runtime-auth-health.test.d.ts +1 -0
- package/bundle/shared/dist/runtime-auth-health.test.js +200 -0
- package/bundle/shared/dist/runtime-availability.d.ts +51 -0
- package/bundle/shared/dist/runtime-availability.js +13 -0
- package/bundle/shared/dist/runtime.d.ts +1 -1
- package/bundle/shared/dist/runtime.js +1 -1
- package/bundle/shared/dist/usage-events.d.ts +36 -0
- package/bundle/shared/dist/usage-events.js +13 -0
- package/bundle/shared/dist/worker-sessions.d.ts +140 -0
- package/bundle/shared/dist/worker-sessions.js +70 -0
- package/bundle/shared/dist/worker-sessions.test.d.ts +1 -0
- package/bundle/shared/dist/worker-sessions.test.js +39 -0
- package/bundle/shared/dist/workflow.d.ts +78 -0
- package/bundle/shared/dist/workflow.js +59 -0
- package/bundle/shared/dist/workflow.test.d.ts +1 -0
- package/bundle/shared/dist/workflow.test.js +61 -0
- package/bundle/shared/package.json +1 -1
- package/dist/action.d.ts +1 -0
- package/dist/action.js +50 -0
- package/dist/action.test.d.ts +1 -0
- package/dist/action.test.js +70 -0
- package/dist/agent.d.ts +1 -0
- package/dist/agent.js +17 -0
- package/dist/agent.test.d.ts +1 -0
- package/dist/agent.test.js +19 -0
- package/dist/cli-check.d.ts +7 -0
- package/dist/cli-check.js +40 -0
- package/dist/doctor.js +59 -1
- package/dist/http.d.ts +5 -0
- package/dist/http.js +19 -0
- package/dist/index.js +28 -0
- package/dist/issue.d.ts +35 -0
- package/dist/issue.js +143 -0
- package/dist/issue.test.d.ts +1 -0
- package/dist/issue.test.js +240 -0
- package/dist/lifecycle.contract.test.d.ts +1 -0
- package/dist/lifecycle.contract.test.js +135 -0
- package/dist/queue.d.ts +11 -0
- package/dist/queue.js +60 -0
- package/dist/queue.test.d.ts +1 -0
- package/dist/queue.test.js +46 -0
- package/dist/test-fetch-stub.d.ts +6 -0
- package/dist/test-fetch-stub.js +31 -0
- package/package.json +2 -1
- package/templates/prod.env.example +1 -1
- package/bundle/server/static-ui/assets/index-B_7S7xJb.js +0 -79
- package/bundle/server/static-ui/assets/index-QYv3zmE6.css +0 -1
|
@@ -7,27 +7,26 @@ process.env.AGENT_DEALER_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-se
|
|
|
7
7
|
process.env.MAX_CONCURRENT_RUNS = "0";
|
|
8
8
|
const { migrate } = await import("../db/index.js");
|
|
9
9
|
const { BUILTIN_AGENT_CLAUDE_ID } = await import("@agent-dealer/shared");
|
|
10
|
-
const {
|
|
11
|
-
const {
|
|
12
|
-
const {
|
|
13
|
-
const {
|
|
14
|
-
const
|
|
10
|
+
const { addArtifact, createRun, getLatestArtifact, getRun, transitionRun, updateRunFields } = await import("../repository/runs.js");
|
|
11
|
+
const { approveRunWithDeliver, resolveOutboundDeliveryAction, resolveOpenDeliveryParkForRun } = await import("./approve-deliver.js");
|
|
12
|
+
const { rejectPendingOutboundDrafts, pendingSendCount, incrementOutboundDeliveryAttempt } = await import("../repository/outbound-drafts.js");
|
|
13
|
+
const { findOpenHumanActionForRun, listHumanActionsForRun, getHumanAction } = await import("../repository/human-actions.js");
|
|
14
|
+
const DECK = "6e825b59-13de-4ddd-ab7e-55ab5a1c279a";
|
|
15
15
|
const TOOL_CALL = {
|
|
16
16
|
serviceName: "34eb6c24-f151-4da2-8db8-d6996aa296be",
|
|
17
17
|
toolName: "chat_postMessage",
|
|
18
|
-
arguments: { channel: "C1", text: "Hello gate test" }
|
|
18
|
+
arguments: { channel: "C1", text: "Hello gate test" }
|
|
19
19
|
};
|
|
20
20
|
const DRAFT_CONTENT = {
|
|
21
21
|
draft: {
|
|
22
22
|
actionType: "slack_message",
|
|
23
23
|
summary: { target: "#test", body: "Hello gate test" },
|
|
24
|
-
toolCall: TOOL_CALL
|
|
24
|
+
toolCall: TOOL_CALL
|
|
25
25
|
},
|
|
26
|
-
status: "pending"
|
|
26
|
+
status: "pending"
|
|
27
27
|
};
|
|
28
28
|
before(() => {
|
|
29
29
|
migrate();
|
|
30
|
-
updateAgent(BUILTIN_AGENT_CLAUDE_ID, { workspaceRoot: process.env.AGENT_DEALER_HOME });
|
|
31
30
|
});
|
|
32
31
|
function seedReviewRun(withDraft = true) {
|
|
33
32
|
const run = createRun({
|
|
@@ -35,8 +34,9 @@ function seedReviewRun(withDraft = true) {
|
|
|
35
34
|
taskCategory: "communication",
|
|
36
35
|
status: "plan_pending",
|
|
37
36
|
agentId: BUILTIN_AGENT_CLAUDE_ID,
|
|
37
|
+
repo: "acme/app"
|
|
38
38
|
});
|
|
39
|
-
updateRunFields(run.id, { deck_id:
|
|
39
|
+
updateRunFields(run.id, { deck_id: DECK });
|
|
40
40
|
transitionRun(run.id, "plan_approved");
|
|
41
41
|
transitionRun(run.id, "running");
|
|
42
42
|
transitionRun(run.id, "review");
|
|
@@ -56,17 +56,20 @@ test("approve without draft transitions to done", async () => {
|
|
|
56
56
|
assert.equal(res.ok && res.delivered, false);
|
|
57
57
|
assert.equal(getRun(run.id).status, "done");
|
|
58
58
|
});
|
|
59
|
-
test("approve
|
|
59
|
+
test("approve delivers under the run deckId and records a receipt", async () => {
|
|
60
60
|
const run = seedReviewRun(true);
|
|
61
|
+
let capturedDeck = null;
|
|
61
62
|
let captured = null;
|
|
62
63
|
const res = await approveRunWithDeliver(run.id, {
|
|
63
|
-
deliver: async (
|
|
64
|
+
deliver: async (deckId, toolCall) => {
|
|
65
|
+
capturedDeck = deckId;
|
|
64
66
|
captured = toolCall;
|
|
65
|
-
return { toolResult: { ok: true }, permalink: "https://slack.example/msg/1" };
|
|
66
|
-
}
|
|
67
|
+
return { ok: true, toolResult: { ok: true }, permalink: "https://slack.example/msg/1" };
|
|
68
|
+
}
|
|
67
69
|
});
|
|
68
70
|
assert.equal(res.ok, true);
|
|
69
71
|
assert.equal(res.ok && res.delivered, true);
|
|
72
|
+
assert.equal(capturedDeck, DECK);
|
|
70
73
|
assert.deepEqual(captured, TOOL_CALL);
|
|
71
74
|
assert.equal(getRun(run.id).status, "done");
|
|
72
75
|
assert.ok(getLatestArtifact(run.id, "send_receipt"));
|
|
@@ -80,54 +83,170 @@ test("approve with human-edited body sends updated payload", async () => {
|
|
|
80
83
|
outboundBody: "Human tweak before send",
|
|
81
84
|
deliver: async (_deckId, toolCall) => {
|
|
82
85
|
captured = toolCall;
|
|
83
|
-
return { toolResult: { ok: true } };
|
|
84
|
-
}
|
|
86
|
+
return { ok: true, toolResult: { ok: true } };
|
|
87
|
+
}
|
|
85
88
|
});
|
|
86
89
|
assert.equal(res.ok, true);
|
|
87
90
|
assert.deepEqual(captured, {
|
|
88
91
|
...TOOL_CALL,
|
|
89
|
-
arguments: { ...TOOL_CALL.arguments, text: "Human tweak before send" }
|
|
92
|
+
arguments: { ...TOOL_CALL.arguments, text: "Human tweak before send" }
|
|
90
93
|
});
|
|
91
94
|
const draftArt = getLatestArtifact(run.id, "slack_draft");
|
|
92
95
|
assert.match(draftArt.contentJson, /Human tweak before send/);
|
|
93
96
|
});
|
|
94
|
-
test("deliver failure keeps run in review with pending draft", async () => {
|
|
97
|
+
test("deliver infra failure keeps run in review with pending draft, no park", async () => {
|
|
95
98
|
const run = seedReviewRun(true);
|
|
96
99
|
const res = await approveRunWithDeliver(run.id, {
|
|
97
|
-
deliver: async () => {
|
|
98
|
-
throw new Error("deck down");
|
|
99
|
-
},
|
|
100
|
+
deliver: async () => ({ ok: false, kind: "infra_failure", reason: "deck down" })
|
|
100
101
|
});
|
|
101
102
|
assert.equal(res.ok, false);
|
|
102
103
|
assert.equal(!res.ok && res.code, 502);
|
|
104
|
+
assert.equal(!res.ok && res.errorCode, "DELIVERY_FAILED");
|
|
103
105
|
assert.equal(getRun(run.id).status, "review");
|
|
104
106
|
assert.equal(pendingSendCount(run.id), 1);
|
|
107
|
+
assert.equal(findOpenHumanActionForRun(run.id, "outbound_delivery_interaction_required"), null);
|
|
105
108
|
const draftArt = getLatestArtifact(run.id, "slack_draft");
|
|
106
109
|
assert.match(draftArt.contentJson, /"status":"pending"/);
|
|
107
110
|
});
|
|
108
|
-
test("
|
|
111
|
+
test("off-scope/provider denial surfaces as an ordinary delivery failure, not parked", async () => {
|
|
112
|
+
const run = seedReviewRun(true);
|
|
113
|
+
const res = await approveRunWithDeliver(run.id, {
|
|
114
|
+
deliver: async () => ({
|
|
115
|
+
ok: false,
|
|
116
|
+
kind: "infra_failure",
|
|
117
|
+
reason: "RESOURCE_OUT_OF_SCOPE: tool not in authority's allowed set"
|
|
118
|
+
})
|
|
119
|
+
});
|
|
120
|
+
assert.equal(res.ok, false);
|
|
121
|
+
assert.equal(getRun(run.id).status, "review");
|
|
122
|
+
assert.equal(findOpenHumanActionForRun(run.id, "outbound_delivery_interaction_required"), null);
|
|
123
|
+
assert.ok(!getLatestArtifact(run.id, "send_receipt"));
|
|
124
|
+
});
|
|
125
|
+
test("deliver returning ambiguous parks the run: draft pending, run in review, one open action", async () => {
|
|
109
126
|
const run = seedReviewRun(true);
|
|
110
|
-
let deliverCalls = 0;
|
|
111
127
|
const res = await approveRunWithDeliver(run.id, {
|
|
128
|
+
deliver: async () => ({
|
|
129
|
+
ok: false,
|
|
130
|
+
kind: "ambiguous",
|
|
131
|
+
reason: "Outbound deliver timed out after 60000ms — whether the message was actually sent is unknown"
|
|
132
|
+
})
|
|
133
|
+
});
|
|
134
|
+
assert.equal(res.ok, false);
|
|
135
|
+
assert.equal(!res.ok && res.errorCode, "AMBIGUOUS_RESULT");
|
|
136
|
+
assert.equal(getRun(run.id).status, "review");
|
|
137
|
+
assert.equal(pendingSendCount(run.id), 1);
|
|
138
|
+
const action = findOpenHumanActionForRun(run.id, "outbound_delivery_interaction_required");
|
|
139
|
+
assert.ok(action);
|
|
140
|
+
assert.equal(action.runId, run.id);
|
|
141
|
+
assert.deepEqual(JSON.parse(action.responseOptionsJson), [
|
|
142
|
+
{ choice: "retry_send", label: "Retry send" },
|
|
143
|
+
{ choice: "reject", label: "Reject draft" },
|
|
144
|
+
]);
|
|
145
|
+
});
|
|
146
|
+
test("a repeated ambiguous signal dedupes to one action", async () => {
|
|
147
|
+
const run = seedReviewRun(true);
|
|
148
|
+
const deps = {
|
|
149
|
+
deliver: async () => ({
|
|
150
|
+
ok: false,
|
|
151
|
+
kind: "ambiguous",
|
|
152
|
+
reason: "timeout — whether the message was actually sent is unknown"
|
|
153
|
+
})
|
|
154
|
+
};
|
|
155
|
+
await approveRunWithDeliver(run.id, deps);
|
|
156
|
+
await approveRunWithDeliver(run.id, deps);
|
|
157
|
+
const actions = listHumanActionsForRun(run.id).filter((a) => a.actionType === "outbound_delivery_interaction_required");
|
|
158
|
+
assert.equal(actions.length, 1);
|
|
159
|
+
});
|
|
160
|
+
async function seedParkedRunViaAmbiguous() {
|
|
161
|
+
const run = seedReviewRun(true);
|
|
162
|
+
await approveRunWithDeliver(run.id, {
|
|
163
|
+
deliver: async () => ({
|
|
164
|
+
ok: false,
|
|
165
|
+
kind: "ambiguous",
|
|
166
|
+
reason: "timeout — whether the message was actually sent is unknown"
|
|
167
|
+
})
|
|
168
|
+
});
|
|
169
|
+
const action = findOpenHumanActionForRun(run.id, "outbound_delivery_interaction_required");
|
|
170
|
+
assert.ok(action, "expected the original attempt to have parked the run");
|
|
171
|
+
return { run, actionId: action.id };
|
|
172
|
+
}
|
|
173
|
+
test("resolveOutboundDeliveryAction:retry_send re-delivers and closes the park on success", async () => {
|
|
174
|
+
const { run, actionId } = await seedParkedRunViaAmbiguous();
|
|
175
|
+
const res = await resolveOutboundDeliveryAction(actionId, "yusuke", "retry_send", {
|
|
176
|
+
deliver: async () => ({ ok: true, toolResult: { ok: true } })
|
|
177
|
+
});
|
|
178
|
+
assert.equal(res.ok, true);
|
|
179
|
+
assert.equal(res.ok && res.delivered, true);
|
|
180
|
+
assert.equal(getRun(run.id).status, "done");
|
|
181
|
+
const resolved = getHumanAction(actionId);
|
|
182
|
+
assert.equal(resolved.status, "resolved");
|
|
183
|
+
assert.equal(resolved.resolvedBy, "yusuke");
|
|
184
|
+
assert.deepEqual(JSON.parse(resolved.resolutionJson), { choice: "retry_send" });
|
|
185
|
+
});
|
|
186
|
+
test("a plain re-approve (not via retry_send) closes an open delivery park too, with a generic system resolution", async () => {
|
|
187
|
+
const { run, actionId } = await seedParkedRunViaAmbiguous();
|
|
188
|
+
const res = await approveRunWithDeliver(run.id, {
|
|
189
|
+
deliver: async () => ({ ok: true, toolResult: { ok: true } })
|
|
190
|
+
});
|
|
191
|
+
assert.equal(res.ok, true);
|
|
192
|
+
assert.equal(res.ok && res.delivered, true);
|
|
193
|
+
assert.equal(getRun(run.id).status, "done");
|
|
194
|
+
const resolved = getHumanAction(actionId);
|
|
195
|
+
assert.equal(resolved.status, "resolved");
|
|
196
|
+
assert.equal(resolved.resolvedBy, "system");
|
|
197
|
+
assert.deepEqual(JSON.parse(resolved.resolutionJson), { choice: "resolved_via_approve" });
|
|
198
|
+
});
|
|
199
|
+
test("resolveOutboundDeliveryAction:retry_send that fails leaves the action open, not resolved", async () => {
|
|
200
|
+
const { run, actionId } = await seedParkedRunViaAmbiguous();
|
|
201
|
+
const res = await resolveOutboundDeliveryAction(actionId, "yusuke", "retry_send", {
|
|
202
|
+
deliver: async () => ({ ok: false, kind: "infra_failure", reason: "deck down again" })
|
|
203
|
+
});
|
|
204
|
+
assert.equal(res.ok, false);
|
|
205
|
+
assert.equal(getRun(run.id).status, "review");
|
|
206
|
+
assert.equal(pendingSendCount(run.id), 1);
|
|
207
|
+
assert.equal(getHumanAction(actionId).status, "open");
|
|
208
|
+
});
|
|
209
|
+
test("resolveOutboundDeliveryAction:retry_send that hits ambiguous again reuses the same open action, no duplicate", async () => {
|
|
210
|
+
const { run, actionId } = await seedParkedRunViaAmbiguous();
|
|
211
|
+
const res = await resolveOutboundDeliveryAction(actionId, "yusuke", "retry_send", {
|
|
212
|
+
deliver: async () => ({ ok: false, kind: "ambiguous", reason: "still ambiguous" })
|
|
213
|
+
});
|
|
214
|
+
assert.equal(res.ok, false);
|
|
215
|
+
assert.equal(getHumanAction(actionId).status, "open");
|
|
216
|
+
const actions = listHumanActionsForRun(run.id).filter((a) => a.actionType === "outbound_delivery_interaction_required");
|
|
217
|
+
assert.equal(actions.length, 1);
|
|
218
|
+
assert.equal(actions[0].id, actionId);
|
|
219
|
+
});
|
|
220
|
+
test("resolveOutboundDeliveryAction:reject makes no deliver call, draft ends rejected, run ends done", async () => {
|
|
221
|
+
const { run, actionId } = await seedParkedRunViaAmbiguous();
|
|
222
|
+
let called = false;
|
|
223
|
+
const res = await resolveOutboundDeliveryAction(actionId, "yusuke", "reject", {
|
|
112
224
|
deliver: async () => {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
225
|
+
called = true;
|
|
226
|
+
throw new Error("must not be called");
|
|
227
|
+
}
|
|
116
228
|
});
|
|
117
229
|
assert.equal(res.ok, true);
|
|
118
|
-
assert.equal(
|
|
230
|
+
assert.equal(res.ok && res.delivered, false);
|
|
231
|
+
assert.equal(called, false);
|
|
232
|
+
assert.equal(pendingSendCount(run.id), 0);
|
|
233
|
+
assert.equal(getRun(run.id).status, "done");
|
|
119
234
|
const draftArt = getLatestArtifact(run.id, "slack_draft");
|
|
120
|
-
assert.match(draftArt.contentJson, /"status":"
|
|
235
|
+
assert.match(draftArt.contentJson, /"status":"rejected"/);
|
|
121
236
|
});
|
|
122
|
-
test("
|
|
237
|
+
test("mark sent before deliver prevents double-send race", async () => {
|
|
123
238
|
const run = seedReviewRun(true);
|
|
239
|
+
let deliverCalls = 0;
|
|
124
240
|
const res = await approveRunWithDeliver(run.id, {
|
|
125
241
|
deliver: async () => {
|
|
126
|
-
|
|
127
|
-
|
|
242
|
+
deliverCalls++;
|
|
243
|
+
return { ok: true, toolResult: { ok: true } };
|
|
244
|
+
}
|
|
128
245
|
});
|
|
129
|
-
assert.equal(res.ok,
|
|
130
|
-
assert.equal(
|
|
246
|
+
assert.equal(res.ok, true);
|
|
247
|
+
assert.equal(deliverCalls, 1);
|
|
248
|
+
const draftArt = getLatestArtifact(run.id, "slack_draft");
|
|
249
|
+
assert.match(draftArt.contentJson, /"status":"sent"/);
|
|
131
250
|
});
|
|
132
251
|
test("retry rejects pending draft", () => {
|
|
133
252
|
const run = seedReviewRun(true);
|
|
@@ -136,7 +255,24 @@ test("retry rejects pending draft", () => {
|
|
|
136
255
|
const draftArt = getLatestArtifact(run.id, "slack_draft");
|
|
137
256
|
assert.match(draftArt.contentJson, /"status":"rejected"/);
|
|
138
257
|
});
|
|
139
|
-
test("
|
|
140
|
-
const
|
|
141
|
-
|
|
258
|
+
test("resolveOpenDeliveryParkForRun closes an open park", async () => {
|
|
259
|
+
const { run, actionId } = await seedParkedRunViaAmbiguous();
|
|
260
|
+
resolveOpenDeliveryParkForRun(run.id, { resolvedBy: "system", choice: "superseded_by_retry" });
|
|
261
|
+
const resolved = getHumanAction(actionId);
|
|
262
|
+
assert.equal(resolved.status, "resolved");
|
|
263
|
+
assert.equal(resolved.resolvedBy, "system");
|
|
264
|
+
assert.deepEqual(JSON.parse(resolved.resolutionJson), { choice: "superseded_by_retry" });
|
|
265
|
+
});
|
|
266
|
+
test("resolveOpenDeliveryParkForRun is a no-op when nothing is parked", () => {
|
|
267
|
+
const run = seedReviewRun(true);
|
|
268
|
+
resolveOpenDeliveryParkForRun(run.id, { resolvedBy: "system", choice: "cancelled" });
|
|
269
|
+
});
|
|
270
|
+
test("incrementOutboundDeliveryAttempt counts up from 1 for a real draft", () => {
|
|
271
|
+
const run = seedReviewRun(true);
|
|
272
|
+
const draftArt = getLatestArtifact(run.id, "slack_draft");
|
|
273
|
+
assert.equal(incrementOutboundDeliveryAttempt(draftArt.id), 1);
|
|
274
|
+
assert.equal(incrementOutboundDeliveryAttempt(draftArt.id), 2);
|
|
275
|
+
});
|
|
276
|
+
test("incrementOutboundDeliveryAttempt returns null for a nonexistent artifact", () => {
|
|
277
|
+
assert.equal(incrementOutboundDeliveryAttempt("00000000-0000-0000-0000-000000000000"), null);
|
|
142
278
|
});
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import { serializePhaseBudget } from "@agent-dealer/shared";
|
|
1
|
+
import { ReasoningEffort as ReasoningEffortSchema, resolveProfileBudgetJson, resolveProfileModel, serializePermissionPolicyOverride, serializePhaseBudget, } from "@agent-dealer/shared";
|
|
2
2
|
import { v4 as uuid } from "uuid";
|
|
3
3
|
import { getDb } from "../db/index.js";
|
|
4
|
+
function parseStoredEffort(raw) {
|
|
5
|
+
if (!raw)
|
|
6
|
+
return null;
|
|
7
|
+
const parsed = ReasoningEffortSchema.safeParse(raw);
|
|
8
|
+
return parsed.success ? parsed.data : null;
|
|
9
|
+
}
|
|
4
10
|
function rowToAgent(row) {
|
|
5
11
|
return {
|
|
6
12
|
id: row.id,
|
|
@@ -10,10 +16,21 @@ function rowToAgent(row) {
|
|
|
10
16
|
deckId: row.deck_id,
|
|
11
17
|
deckName: row.deck_name,
|
|
12
18
|
playbookId: row.playbook_id,
|
|
19
|
+
// Read-only legacy compatibility (NOT-71): no write path sets these any more, but
|
|
20
|
+
// profile-snapshot.ts falls back to them for profiles saved before default_model /
|
|
21
|
+
// default_budget_json existed, so dropping them here would silently change the model
|
|
22
|
+
// and caps those sessions run under.
|
|
13
23
|
defaultPlanModel: row.default_plan_model,
|
|
14
24
|
defaultExecuteModel: row.default_execute_model,
|
|
15
25
|
defaultPlanBudgetJson: row.default_plan_budget_json,
|
|
16
26
|
defaultExecuteBudgetJson: row.default_execute_budget_json,
|
|
27
|
+
defaultModel: row.default_model,
|
|
28
|
+
defaultEffort: parseStoredEffort(row.default_effort),
|
|
29
|
+
defaultBudgetJson: row.default_budget_json,
|
|
30
|
+
purpose: row.purpose,
|
|
31
|
+
playbookIdsJson: row.playbook_ids_json,
|
|
32
|
+
externalMemoryRefsJson: row.external_memory_refs_json,
|
|
33
|
+
permissionPolicyJson: row.permission_policy_json,
|
|
17
34
|
isBuiltin: row.is_builtin === 1,
|
|
18
35
|
createdAt: row.created_at,
|
|
19
36
|
updatedAt: row.updated_at,
|
|
@@ -34,9 +51,17 @@ export function createAgent(input, deckName) {
|
|
|
34
51
|
const now = new Date().toISOString();
|
|
35
52
|
const id = uuid();
|
|
36
53
|
db.prepare(`
|
|
37
|
-
INSERT INTO agents (
|
|
38
|
-
|
|
39
|
-
|
|
54
|
+
INSERT INTO agents (
|
|
55
|
+
id, name, runtime, deck_id, deck_name, playbook_id, workspace_root,
|
|
56
|
+
default_model, default_effort, default_budget_json, purpose, playbook_ids_json, external_memory_refs_json, permission_policy_json,
|
|
57
|
+
is_builtin, created_at, updated_at
|
|
58
|
+
)
|
|
59
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)
|
|
60
|
+
`).run(id, input.name.trim(), input.runtime, input.deckId, deckName ?? null, null, // playbook_id — dead legacy (NOT-149)
|
|
61
|
+
null, // workspace_root — dead legacy (NOT-149)
|
|
62
|
+
input.defaultModel ?? null, input.defaultEffort ?? null, serializePhaseBudget(input.defaultBudget), input.purpose?.trim() || null, null, // playbook_ids_json — dead legacy
|
|
63
|
+
null, // external_memory_refs_json — dead legacy
|
|
64
|
+
serializePermissionPolicyOverride(input.permissionPolicy), now, now);
|
|
40
65
|
return getAgent(id);
|
|
41
66
|
}
|
|
42
67
|
export function updateAgent(id, input, deckName) {
|
|
@@ -46,25 +71,39 @@ export function updateAgent(id, input, deckName) {
|
|
|
46
71
|
const now = new Date().toISOString();
|
|
47
72
|
const name = input.name?.trim() ?? existing.name;
|
|
48
73
|
const runtime = input.runtime ?? existing.runtime;
|
|
49
|
-
|
|
74
|
+
// workspace / playbook / external-memory columns are dead legacy (NOT-149) — leave
|
|
75
|
+
// whatever was stored so in-flight migration stays recoverable; never rewrite from API.
|
|
76
|
+
const workspaceRoot = existing.workspaceRoot;
|
|
50
77
|
const deckId = input.deckId !== undefined ? input.deckId : existing.deckId;
|
|
51
|
-
const playbookId =
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
78
|
+
const playbookId = existing.playbookId;
|
|
79
|
+
// Collapse the pre-NOT-71 plan/execute columns into the role-neutral one on every write,
|
|
80
|
+
// and clear them below. Reading them back (resolveProfile*) is deliberate compatibility for
|
|
81
|
+
// rows written before the migration; continuing to *keep* them is not. Without this, editing
|
|
82
|
+
// a legacy profile saved a null role-neutral model while the legacy column kept winning the
|
|
83
|
+
// fallback — so the form showed blank while the session still ran the hidden value, and
|
|
84
|
+
// switching runtime carried the old runtime's model into the new one's snapshot.
|
|
85
|
+
const defaultModel = input.defaultModel !== undefined ? input.defaultModel : resolveProfileModel(existing);
|
|
86
|
+
const defaultEffort = input.defaultEffort !== undefined ? input.defaultEffort : existing.defaultEffort;
|
|
87
|
+
const defaultBudgetJson = input.defaultBudget !== undefined
|
|
88
|
+
? serializePhaseBudget(input.defaultBudget)
|
|
89
|
+
: resolveProfileBudgetJson(existing);
|
|
90
|
+
const purpose = input.purpose !== undefined ? input.purpose?.trim() || null : existing.purpose;
|
|
91
|
+
const playbookIdsJson = existing.playbookIdsJson;
|
|
92
|
+
const externalMemoryRefsJson = existing.externalMemoryRefsJson;
|
|
93
|
+
const permissionPolicyJson = input.permissionPolicy !== undefined
|
|
94
|
+
? serializePermissionPolicyOverride(input.permissionPolicy)
|
|
95
|
+
: existing.permissionPolicyJson;
|
|
60
96
|
const resolvedDeckName = input.deckId !== undefined ? (input.deckId ? (deckName ?? null) : null) : existing.deckName;
|
|
61
97
|
getDb()
|
|
62
98
|
.prepare(`
|
|
63
99
|
UPDATE agents SET name = ?, runtime = ?, deck_id = ?, deck_name = ?, playbook_id = ?, workspace_root = ?,
|
|
64
|
-
|
|
100
|
+
default_model = ?, default_effort = ?, default_budget_json = ?, purpose = ?, playbook_ids_json = ?, external_memory_refs_json = ?, permission_policy_json = ?,
|
|
101
|
+
default_plan_model = NULL, default_execute_model = NULL,
|
|
102
|
+
default_plan_budget_json = NULL, default_execute_budget_json = NULL,
|
|
103
|
+
updated_at = ?
|
|
65
104
|
WHERE id = ?
|
|
66
105
|
`)
|
|
67
|
-
.run(name, runtime, deckId, resolvedDeckName, playbookId, workspaceRoot,
|
|
106
|
+
.run(name, runtime, deckId, resolvedDeckName, playbookId, workspaceRoot, defaultModel, defaultEffort, defaultBudgetJson, purpose, playbookIdsJson, externalMemoryRefsJson, permissionPolicyJson, now, id);
|
|
68
107
|
return getAgent(id);
|
|
69
108
|
}
|
|
70
109
|
export function deleteAgent(id) {
|
|
@@ -82,9 +121,7 @@ export function resolveAgent(agentId) {
|
|
|
82
121
|
agentId: agent.id,
|
|
83
122
|
agentName: agent.name,
|
|
84
123
|
runtime: agent.runtime,
|
|
85
|
-
workspaceRoot: agent.workspaceRoot ?? undefined,
|
|
86
124
|
deckId: agent.deckId ?? undefined,
|
|
87
125
|
deckName: agent.deckName,
|
|
88
|
-
playbookId: agent.playbookId ?? undefined,
|
|
89
126
|
};
|
|
90
127
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { getDb } from "../db/index.js";
|
|
2
|
+
function rowToArtifact(row) {
|
|
3
|
+
return {
|
|
4
|
+
id: row.id,
|
|
5
|
+
issueId: row.issue_id,
|
|
6
|
+
workerSessionId: row.worker_session_id,
|
|
7
|
+
kind: row.kind,
|
|
8
|
+
contentJson: row.content_json,
|
|
9
|
+
blobPath: row.blob_path,
|
|
10
|
+
author: row.author,
|
|
11
|
+
createdAt: row.created_at,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/** Scoped to the issue so a caller can't fetch an artifact belonging to a different
|
|
15
|
+
* issue by guessing/incrementing an id — used by the raw-trace route. */
|
|
16
|
+
export function getIssueArtifact(issueId, artifactId) {
|
|
17
|
+
const row = getDb()
|
|
18
|
+
.prepare("SELECT * FROM artifacts WHERE id = ? AND issue_id = ?")
|
|
19
|
+
.get(artifactId, issueId);
|
|
20
|
+
return row ? rowToArtifact(row) : null;
|
|
21
|
+
}
|
|
22
|
+
export function listArtifactsForIssue(issueId, opts) {
|
|
23
|
+
const limit = opts?.limit ?? 50;
|
|
24
|
+
const rows = opts?.before
|
|
25
|
+
? getDb()
|
|
26
|
+
.prepare("SELECT * FROM artifacts WHERE issue_id = ? AND created_at < ? ORDER BY created_at DESC LIMIT ?")
|
|
27
|
+
.all(issueId, opts.before, limit)
|
|
28
|
+
: getDb()
|
|
29
|
+
.prepare("SELECT * FROM artifacts WHERE issue_id = ? ORDER BY created_at DESC LIMIT ?")
|
|
30
|
+
.all(issueId, limit);
|
|
31
|
+
return rows.map(rowToArtifact);
|
|
32
|
+
}
|
|
33
|
+
/** Every artifact of `kind` for the issue, unbounded — unlike `listArtifactsForIssue`'s
|
|
34
|
+
* newest-first `limit` window, filtering by kind in SQL means a caller that needs "all
|
|
35
|
+
* artifacts of this kind ever recorded" (e.g. reflect-trigger's already-proposed-playbook
|
|
36
|
+
* scan) can't have an old row fall outside the window on an artifact-heavy issue. */
|
|
37
|
+
export function listArtifactsForIssueByKind(issueId, kind) {
|
|
38
|
+
const rows = getDb()
|
|
39
|
+
.prepare("SELECT * FROM artifacts WHERE issue_id = ? AND kind = ? ORDER BY created_at DESC")
|
|
40
|
+
.all(issueId, kind);
|
|
41
|
+
return rows.map(rowToArtifact);
|
|
42
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { test, before } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
process.env.AGENT_DEALER_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-artifacts-issue-"));
|
|
7
|
+
const { migrate, getDb } = await import("../db/index.js");
|
|
8
|
+
const { BUILTIN_AGENT_CLAUDE_ID, BUILTIN_AGENT_CURSOR_ID } = await import("@agent-dealer/shared");
|
|
9
|
+
const { createIssue } = await import("./issues.js");
|
|
10
|
+
const { listArtifactsForIssue, listArtifactsForIssueByKind } = await import("./artifacts-for-issue.js");
|
|
11
|
+
const { recordUsageEvent } = await import("./usage-events.js");
|
|
12
|
+
const { createWorkerSession } = await import("./worker-sessions.js");
|
|
13
|
+
before(() => {
|
|
14
|
+
migrate();
|
|
15
|
+
});
|
|
16
|
+
test("lists artifacts for an issue newest first, respects limit", () => {
|
|
17
|
+
const issue = createIssue({ title: "T", repo: "acme/app", baseBranch: "main", developerAgentId: BUILTIN_AGENT_CLAUDE_ID, reviewerAgentId: BUILTIN_AGENT_CURSOR_ID, maxReviewRounds: 3, maxInfraAttempts: 3, source: "manual" });
|
|
18
|
+
const db = getDb();
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
for (let i = 0; i < 3; i++) {
|
|
21
|
+
db.prepare(`INSERT INTO artifacts (id, issue_id, run_id, kind, content_json, author, created_at) VALUES (?, ?, NULL, 'task_snapshot', '{}', 'system', ?)`).run(`art-${i}`, issue.id, new Date(now + i * 1000).toISOString());
|
|
22
|
+
}
|
|
23
|
+
const all = listArtifactsForIssue(issue.id);
|
|
24
|
+
assert.equal(all.length, 3);
|
|
25
|
+
assert.equal(all[0].id, "art-2"); // newest first
|
|
26
|
+
const limited = listArtifactsForIssue(issue.id, { limit: 2 });
|
|
27
|
+
assert.equal(limited.length, 2);
|
|
28
|
+
});
|
|
29
|
+
test("listArtifactsForIssueByKind finds a matching row regardless of how many newer rows of other kinds exist", () => {
|
|
30
|
+
const issue = createIssue({ title: "K", repo: "acme/app", baseBranch: "main", developerAgentId: BUILTIN_AGENT_CLAUDE_ID, reviewerAgentId: BUILTIN_AGENT_CURSOR_ID, maxReviewRounds: 3, maxInfraAttempts: 3, source: "manual" });
|
|
31
|
+
const db = getDb();
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
db.prepare(`INSERT INTO artifacts (id, issue_id, run_id, kind, content_json, author, created_at) VALUES (?, ?, NULL, 'playbook_patch', '{}', 'system', ?)`).run("patch-1", issue.id, new Date(now).toISOString());
|
|
34
|
+
for (let i = 0; i < 250; i++) {
|
|
35
|
+
db.prepare(`INSERT INTO artifacts (id, issue_id, run_id, kind, content_json, author, created_at) VALUES (?, ?, NULL, 'reflect_status', '{}', 'system', ?)`).run(`status-${i}`, issue.id, new Date(now + (i + 1) * 1000).toISOString());
|
|
36
|
+
}
|
|
37
|
+
const matches = listArtifactsForIssueByKind(issue.id, "playbook_patch");
|
|
38
|
+
assert.equal(matches.length, 1);
|
|
39
|
+
assert.equal(matches[0].id, "patch-1");
|
|
40
|
+
});
|
|
41
|
+
test("listUsageEventsForIssue returns recorded events", async () => {
|
|
42
|
+
const { listUsageEventsForIssue } = await import("./usage-events.js");
|
|
43
|
+
const issue = createIssue({ title: "U", repo: "acme/app", baseBranch: "main", developerAgentId: BUILTIN_AGENT_CLAUDE_ID, reviewerAgentId: BUILTIN_AGENT_CURSOR_ID, maxReviewRounds: 3, maxInfraAttempts: 3, source: "manual" });
|
|
44
|
+
const session = createWorkerSession({ issueId: issue.id, role: "developer", round: 1, agentId: BUILTIN_AGENT_CLAUDE_ID, runtime: "claude_code" });
|
|
45
|
+
recordUsageEvent({ issueId: issue.id, workerSessionId: session.id, role: "developer", costUsd: 1 });
|
|
46
|
+
const events = listUsageEventsForIssue(issue.id);
|
|
47
|
+
assert.equal(events.length, 1);
|
|
48
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// packages/server/src/repository/artifacts.ts
|
|
2
|
+
//
|
|
3
|
+
// Write side for issue-scoped artifacts. `artifacts-for-issue.ts` only ever read this
|
|
4
|
+
// table (issue_id/worker_session_id columns added additively by migrate(), see
|
|
5
|
+
// db/index.ts) — nothing wrote an issue-scoped row until NOT-61 needed one for the frozen
|
|
6
|
+
// task snapshot and the developer's implementation conclusion.
|
|
7
|
+
import { v4 as uuid } from "uuid";
|
|
8
|
+
import { getDb } from "../db/index.js";
|
|
9
|
+
export function createIssueArtifact(input) {
|
|
10
|
+
const id = uuid();
|
|
11
|
+
const now = new Date().toISOString();
|
|
12
|
+
const contentJson = input.content !== undefined ? JSON.stringify(input.content) : null;
|
|
13
|
+
getDb()
|
|
14
|
+
.prepare(`
|
|
15
|
+
INSERT INTO artifacts (id, issue_id, worker_session_id, kind, content_json, blob_path, author, created_at)
|
|
16
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
17
|
+
`)
|
|
18
|
+
.run(id, input.issueId, input.workerSessionId ?? null, input.kind, contentJson, input.blobPath ?? null, input.author, now);
|
|
19
|
+
return {
|
|
20
|
+
id,
|
|
21
|
+
issueId: input.issueId,
|
|
22
|
+
workerSessionId: input.workerSessionId ?? null,
|
|
23
|
+
kind: input.kind,
|
|
24
|
+
contentJson,
|
|
25
|
+
blobPath: input.blobPath ?? null,
|
|
26
|
+
author: input.author,
|
|
27
|
+
createdAt: now,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Most recent artifact of `kind` for the issue — used to read the frozen task snapshot. */
|
|
31
|
+
export function latestIssueArtifact(issueId, kind) {
|
|
32
|
+
const row = getDb()
|
|
33
|
+
.prepare("SELECT * FROM artifacts WHERE issue_id = ? AND kind = ? ORDER BY created_at DESC LIMIT 1")
|
|
34
|
+
.get(issueId, kind);
|
|
35
|
+
if (!row)
|
|
36
|
+
return null;
|
|
37
|
+
return {
|
|
38
|
+
id: row.id,
|
|
39
|
+
issueId: row.issue_id,
|
|
40
|
+
workerSessionId: row.worker_session_id,
|
|
41
|
+
kind: row.kind,
|
|
42
|
+
contentJson: row.content_json,
|
|
43
|
+
blobPath: row.blob_path,
|
|
44
|
+
author: row.author,
|
|
45
|
+
createdAt: row.created_at,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// packages/server/src/repository/artifacts.test.ts
|
|
2
|
+
import { test, before } from "node:test";
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
process.env.AGENT_DEALER_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-artifacts-"));
|
|
8
|
+
const { migrate } = await import("../db/index.js");
|
|
9
|
+
const { BUILTIN_AGENT_CLAUDE_ID, BUILTIN_AGENT_CURSOR_ID } = await import("@agent-dealer/shared");
|
|
10
|
+
const { createIssue } = await import("./issues.js");
|
|
11
|
+
const { createIssueArtifact, latestIssueArtifact } = await import("./artifacts.js");
|
|
12
|
+
const { listArtifactsForIssue } = await import("./artifacts-for-issue.js");
|
|
13
|
+
before(() => migrate());
|
|
14
|
+
function issue() {
|
|
15
|
+
return createIssue({
|
|
16
|
+
title: "T",
|
|
17
|
+
repo: "acme/app",
|
|
18
|
+
baseBranch: "main",
|
|
19
|
+
developerAgentId: BUILTIN_AGENT_CLAUDE_ID,
|
|
20
|
+
reviewerAgentId: BUILTIN_AGENT_CURSOR_ID,
|
|
21
|
+
maxReviewRounds: 3,
|
|
22
|
+
maxInfraAttempts: 3,
|
|
23
|
+
source: "manual"
|
|
24
|
+
}).id;
|
|
25
|
+
}
|
|
26
|
+
test("createIssueArtifact writes a row readable via listArtifactsForIssue", () => {
|
|
27
|
+
const issueId = issue();
|
|
28
|
+
const created = createIssueArtifact({ issueId, kind: "task_snapshot", author: "system", content: { title: "T" } });
|
|
29
|
+
const listed = listArtifactsForIssue(issueId);
|
|
30
|
+
assert.equal(listed.length, 1);
|
|
31
|
+
assert.equal(listed[0].id, created.id);
|
|
32
|
+
assert.equal(listed[0].kind, "task_snapshot");
|
|
33
|
+
assert.deepEqual(JSON.parse(listed[0].contentJson), { title: "T" });
|
|
34
|
+
assert.equal(listed[0].workerSessionId, null);
|
|
35
|
+
});
|
|
36
|
+
test("latestIssueArtifact returns the newest row of that kind, ignoring other kinds", async () => {
|
|
37
|
+
const issueId = issue();
|
|
38
|
+
createIssueArtifact({ issueId, kind: "task_snapshot", author: "system", content: { v: 1 } });
|
|
39
|
+
// created_at is millisecond-resolution — sleep past a tie so ordering is deterministic.
|
|
40
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
41
|
+
const later = createIssueArtifact({ issueId, kind: "task_snapshot", author: "system", content: { v: 2 } });
|
|
42
|
+
createIssueArtifact({ issueId, kind: "implementation_conclusion", author: "agent", content: { text: "done" } });
|
|
43
|
+
const latest = latestIssueArtifact(issueId, "task_snapshot");
|
|
44
|
+
assert.ok(latest);
|
|
45
|
+
assert.equal(latest.id, later.id);
|
|
46
|
+
assert.equal(latest.kind, "task_snapshot");
|
|
47
|
+
assert.equal(latestIssueArtifact(issueId, "no_such_kind"), null);
|
|
48
|
+
});
|