Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions app/Http/Controllers/Api/DaemonController.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use App\Models\Approval;
use App\Models\Server;
use App\Models\Task;
use App\Models\TaskWorkProduct;
use App\Models\UsageEvent;
use App\Services\AuditService;
use App\Services\TaskCheckoutService;
Expand Down Expand Up @@ -170,6 +171,21 @@ public function reportResult(ReportResultRequest $request, string $token, Task $
]);
}

// Create work product records
if (! empty($validated['work_products'])) {
foreach ($validated['work_products'] as $wp) {
TaskWorkProduct::query()->create([
'task_id' => $task->id,
'agent_id' => $task->agent_id,
'type' => $wp['type'] ?? 'file',
'title' => $wp['title'],
'file_path' => $wp['file_path'] ?? null,
'url' => $wp['url'] ?? null,
'summary' => $wp['summary'] ?? null,
]);
}
}

// Process delegations — create sub-tasks for named direct reports
if (! empty($validated['delegations']) && $task->agent?->delegation_enabled) {
foreach ($validated['delegations'] as $delegation) {
Expand Down Expand Up @@ -335,6 +351,33 @@ public function reportUsage(Request $request): JsonResponse
return response()->json(['status' => 'recorded']);
}

/**
* POST /api/daemon/{token}/tasks/{task}/notes
*/
public function postNote(Request $request, string $token, Task $task): JsonResponse
{
/** @var Server $server */
$server = $request->get('daemon_server');

abort_unless(
$task->agent && $task->agent->server_id === $server->id,
403,
'Task agent is not on this server.',
);

$validated = $request->validate([
'body' => ['required', 'string', 'max:5000'],
]);

$note = $task->notes()->create([
'author_type' => 'agent',
'author_id' => $task->agent_id,
'body' => $validated['body'],
]);

return response()->json(['status' => 'ok', 'note' => $note]);
}

/**
* POST /api/daemon/{token}/heartbeat
*/
Expand Down
1 change: 1 addition & 0 deletions app/Http/Controllers/GovernanceTaskController.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ public function show(Request $request, Task $task): Response
'parentTask',
'subTasks.agent',
'usageEvents',
'notes',
]);

$auditEntries = $task->team->auditLogs()
Expand Down
45 changes: 45 additions & 0 deletions app/Http/Controllers/TaskNoteController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace App\Http\Controllers;

use App\Events\TaskStatusChangedEvent;
use App\Models\Task;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;

class TaskNoteController extends Controller
{
/**
* Store a new comment on a task.
*
* If the task is done, reopen it for revision.
*/
public function store(Request $request, Task $task): RedirectResponse
{
abort_unless($task->team_id === $request->user()->current_team_id, 403);

$validated = $request->validate([
'body' => ['required', 'string', 'max:5000'],
]);

$task->notes()->create([
'author_type' => 'user',
'author_id' => $request->user()->id,
'body' => $validated['body'],
]);

if ($task->status === 'done') {
$oldStatus = $task->status;

$task->update([
'status' => 'todo',
'checked_out_by_run' => null,
'checkout_expires_at' => null,
]);

event(new TaskStatusChangedEvent($task->load('agent'), $oldStatus, 'todo'));
}

return redirect()->back();
}
}
33 changes: 33 additions & 0 deletions database/factories/TaskNoteFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace Database\Factories;

use App\Models\Task;
use App\Models\TaskNote;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
* @extends Factory<TaskNote>
*/
class TaskNoteFactory extends Factory
{
/**
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'task_id' => Task::factory(),
'author_type' => 'user',
'author_id' => fake()->uuid(),
'body' => fake()->paragraph(),
];
}

public function byAgent(): static
{
return $this->state(fn (array $attributes) => [
'author_type' => 'agent',
]);
}
}
10 changes: 10 additions & 0 deletions packages/provisiond/bundle/provisiond.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ async function executeTask(task, config, api) {
logger.info(`Skipping task ${taskLabel} \u2014 checkout failed (likely already checked out)`);
return;
}
await api.postNote(task.id, "Starting task...");
try {
const prompt = buildPrompt(task);
const port = task.agent.harness_type === "hermes" ? task.agent.api_server_port : OPENCLAW_DEFAULT_PORT;
Expand Down Expand Up @@ -419,6 +420,15 @@ var ProvisionApiClient = class {
});
}
}
async postNote(taskId, body) {
const res = await this.request("POST", `/tasks/${taskId}/notes`, { body });
if (!res.ok) {
logger.error(`Failed to post note for task ${taskId}`, {
status: res.status,
statusText: res.statusText
});
}
}
async sendHeartbeat(activeRuns2) {
const res = await this.request("POST", "/heartbeat", {
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
Expand Down
2 changes: 1 addition & 1 deletion packages/provisiond/dist/executor.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/provisiond/dist/executor.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/provisiond/dist/executor.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/provisiond/dist/provision-api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export declare class ProvisionApiClient {
releaseTask(taskId: string, runId: string, reason?: string): Promise<void>;
getResolvedApprovals(): Promise<ResolvedApproval[]>;
reportUsage(event: UsageEvent): Promise<void>;
postNote(taskId: string, body: string): Promise<void>;
sendHeartbeat(activeRuns: string[]): Promise<void>;
private request;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/provisiond/dist/provision-api.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions packages/provisiond/dist/provision-api.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading