fifony 0.1.14-next.6f02449 → 0.1.14-next.f20a2d1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const CACHE_VERSION = "
|
|
1
|
+
const CACHE_VERSION = "1773755105602";
|
|
2
2
|
const CORE_CACHE = `fifony-core-${CACHE_VERSION}`;
|
|
3
3
|
const ASSET_CACHE = `fifony-assets-${CACHE_VERSION}`;
|
|
4
4
|
const APP_SHELL_ROUTES = ["/kanban", "/issues", "/agents", "/providers", "/settings"];
|
|
@@ -1147,6 +1147,7 @@ function nextLocalIssueId(issues) {
|
|
|
1147
1147
|
function createIssueFromPayload(payload, issues, workflowDefinition) {
|
|
1148
1148
|
const identifier = toStringValue(payload.identifier, nextLocalIssueId(issues));
|
|
1149
1149
|
const id = toStringValue(payload.id, identifier.replace(/^#/, "issue-"));
|
|
1150
|
+
logger.info({ id, identifier, title: toStringValue(payload.title, "").slice(0, 80) }, "[Issues] Creating new issue");
|
|
1150
1151
|
const createdAt = now();
|
|
1151
1152
|
const blockedBy = toStringArray(payload.blockedBy);
|
|
1152
1153
|
const legacyBlockedBy = toStringArray(payload.blocked_by);
|
|
@@ -1426,6 +1427,7 @@ function addEvent(state, issueId, kind, message) {
|
|
|
1426
1427
|
}
|
|
1427
1428
|
function transition(issue, target, note) {
|
|
1428
1429
|
const previous = issue.state;
|
|
1430
|
+
logger.debug({ issueId: issue.id, identifier: issue.identifier, from: previous, to: target, note }, "[State] Issue transition");
|
|
1429
1431
|
issue.state = target;
|
|
1430
1432
|
issue.updatedAt = now();
|
|
1431
1433
|
markIssueDirty(issue.id);
|
|
@@ -2683,13 +2685,22 @@ function canRunIssue(issue, running, state) {
|
|
|
2683
2685
|
if (running.has(issue.id)) return false;
|
|
2684
2686
|
if (TERMINAL_STATES.has(issue.state)) return false;
|
|
2685
2687
|
const { alive } = isAgentStillRunning(issue);
|
|
2686
|
-
if (alive)
|
|
2688
|
+
if (alive) {
|
|
2689
|
+
logger.debug({ issueId: issue.id, identifier: issue.identifier }, "[Agent] Skipping issue \u2014 agent still alive from previous session");
|
|
2690
|
+
return false;
|
|
2691
|
+
}
|
|
2687
2692
|
if (issue.state === "Blocked") {
|
|
2688
2693
|
if (!issue.nextRetryAt) return false;
|
|
2689
|
-
if (issue.attempts >= issue.maxAttempts)
|
|
2694
|
+
if (issue.attempts >= issue.maxAttempts) {
|
|
2695
|
+
logger.debug({ issueId: issue.id, identifier: issue.identifier, attempts: issue.attempts, maxAttempts: issue.maxAttempts }, "[Agent] Skipping blocked issue \u2014 max attempts reached");
|
|
2696
|
+
return false;
|
|
2697
|
+
}
|
|
2690
2698
|
if (Date.parse(issue.nextRetryAt) > Date.now()) return false;
|
|
2691
2699
|
}
|
|
2692
|
-
if (!issueDepsResolved(issue, state.issues))
|
|
2700
|
+
if (!issueDepsResolved(issue, state.issues)) {
|
|
2701
|
+
logger.debug({ issueId: issue.id, identifier: issue.identifier, blockedBy: issue.blockedBy }, "[Agent] Skipping issue \u2014 unresolved dependencies");
|
|
2702
|
+
return false;
|
|
2703
|
+
}
|
|
2693
2704
|
if (issue.state === "Todo") return true;
|
|
2694
2705
|
if (issue.state === "Queued") return true;
|
|
2695
2706
|
if (issue.state === "Blocked") return true;
|
|
@@ -3194,6 +3205,7 @@ async function runCommandWithTimeout(command, workspacePath, issue, config, prom
|
|
|
3194
3205
|
const pidFile = join8(workspacePath, "fifony-agent.pid");
|
|
3195
3206
|
const pid = child.pid;
|
|
3196
3207
|
if (pid) {
|
|
3208
|
+
logger.debug({ issueId: issue.id, pid, command: command.slice(0, 120), cwd: workspacePath }, "[Agent] Process spawned");
|
|
3197
3209
|
writeFileSync3(pidFile, JSON.stringify({
|
|
3198
3210
|
pid,
|
|
3199
3211
|
issueId: issue.id,
|
|
@@ -3303,6 +3315,7 @@ async function prepareWorkspace(issue, workflowDefinition) {
|
|
|
3303
3315
|
const workspaceRoot = join8(WORKSPACE_ROOT, safeId);
|
|
3304
3316
|
const createdNow = !existsSync5(workspaceRoot);
|
|
3305
3317
|
if (createdNow) {
|
|
3318
|
+
logger.debug({ issueId: issue.id, identifier: issue.identifier, workspacePath: workspaceRoot }, "[Agent] Creating new workspace");
|
|
3306
3319
|
mkdirSync2(workspaceRoot, { recursive: true });
|
|
3307
3320
|
if (workflowDefinition?.afterCreateHook) {
|
|
3308
3321
|
await runHook(workflowDefinition.afterCreateHook, workspaceRoot, issue, "after_create");
|
|
@@ -3314,6 +3327,9 @@ async function prepareWorkspace(issue, workflowDefinition) {
|
|
|
3314
3327
|
filter: (sourcePath) => !sourcePath.startsWith(WORKSPACE_ROOT)
|
|
3315
3328
|
});
|
|
3316
3329
|
}
|
|
3330
|
+
logger.debug({ issueId: issue.id, workspacePath: workspaceRoot }, "[Agent] Workspace created");
|
|
3331
|
+
} else {
|
|
3332
|
+
logger.debug({ issueId: issue.id, workspacePath: workspaceRoot }, "[Agent] Reusing existing workspace");
|
|
3317
3333
|
}
|
|
3318
3334
|
const metaPath = join8(workspaceRoot, "fifony-issue.json");
|
|
3319
3335
|
const promptText = await buildPrompt(issue, workflowDefinition);
|
|
@@ -3338,6 +3354,7 @@ async function runAgentSession(state, issue, provider, cycle, workspacePath, bas
|
|
|
3338
3354
|
let lastOutput = session.lastOutput;
|
|
3339
3355
|
const resultFile = join8(workspacePath, `fifony-result-${provider.role}-${provider.provider}.json`);
|
|
3340
3356
|
if (session.status === "done" && session.turns.length > 0) {
|
|
3357
|
+
logger.debug({ issueId: issue.id, identifier: issue.identifier, provider: provider.provider, role: provider.role }, "[Agent] Session already completed, returning cached result");
|
|
3341
3358
|
return { success: true, blocked: false, continueRequested: false, code: session.lastCode, output: session.lastOutput, turns: session.turns.length };
|
|
3342
3359
|
}
|
|
3343
3360
|
const turnIndex = session.turns.length + 1;
|
|
@@ -3357,6 +3374,7 @@ Agent requested additional turns beyond configured limit (${maxTurns}).`, state.
|
|
|
3357
3374
|
session.lastPromptFile = turnPromptFile;
|
|
3358
3375
|
session.maxTurns = maxTurns;
|
|
3359
3376
|
await persistAgentSessionState(sessionKey, issue, provider, cycle, session);
|
|
3377
|
+
logger.info({ issueId: issue.id, identifier: issue.identifier, turn: turnIndex, maxTurns, provider: provider.provider, role: provider.role, cycle, command: provider.command.slice(0, 120) }, "[Agent] Spawning agent command");
|
|
3360
3378
|
const turnStartedAt = now();
|
|
3361
3379
|
const turnEnv = {
|
|
3362
3380
|
FIFONY_AGENT_PROVIDER: provider.provider,
|
|
@@ -3389,6 +3407,7 @@ Agent requested additional turns beyond configured limit (${maxTurns}).`, state.
|
|
|
3389
3407
|
FIFONY_PRESERVE_RESULT_FILE: "1"
|
|
3390
3408
|
});
|
|
3391
3409
|
}
|
|
3410
|
+
logger.info({ issueId: issue.id, identifier: issue.identifier, turn: turnIndex, exitCode: turnResult.code, success: turnResult.success, outputBytes: turnResult.output.length }, "[Agent] Agent command finished");
|
|
3392
3411
|
const directive = readAgentDirective(workspacePath, turnResult.output, turnResult.success);
|
|
3393
3412
|
lastCode = turnResult.code;
|
|
3394
3413
|
lastOutput = turnResult.output;
|
|
@@ -3434,20 +3453,24 @@ Agent requested additional turns beyond configured limit (${maxTurns}).`, state.
|
|
|
3434
3453
|
const directiveSummary = directive.summary ? ` ${directive.summary}` : "";
|
|
3435
3454
|
addEvent(state, issue.id, "runner", `Turn ${turnIndex}/${maxTurns} finished with status ${directive.status}.${directiveSummary}`.trim());
|
|
3436
3455
|
if (!turnResult.success || directive.status === "failed") {
|
|
3456
|
+
logger.info({ issueId: issue.id, identifier: issue.identifier, turn: turnIndex, directiveStatus: directive.status, exitCode: lastCode }, "[Agent] Session turn failed");
|
|
3437
3457
|
session.status = "failed";
|
|
3438
3458
|
await persistAgentSessionState(sessionKey, issue, provider, cycle, session);
|
|
3439
3459
|
return { success: false, blocked: false, continueRequested: false, code: lastCode, output: lastOutput, turns: turnIndex };
|
|
3440
3460
|
}
|
|
3441
3461
|
if (directive.status === "blocked") {
|
|
3462
|
+
logger.info({ issueId: issue.id, identifier: issue.identifier, turn: turnIndex }, "[Agent] Session turn blocked \u2014 manual intervention requested");
|
|
3442
3463
|
session.status = "blocked";
|
|
3443
3464
|
await persistAgentSessionState(sessionKey, issue, provider, cycle, session);
|
|
3444
3465
|
return { success: false, blocked: true, continueRequested: false, code: lastCode, output: lastOutput, turns: turnIndex };
|
|
3445
3466
|
}
|
|
3446
3467
|
if (directive.status === "continue") {
|
|
3468
|
+
logger.info({ issueId: issue.id, identifier: issue.identifier, turn: turnIndex, maxTurns }, "[Agent] Session requests continuation");
|
|
3447
3469
|
session.status = "running";
|
|
3448
3470
|
await persistAgentSessionState(sessionKey, issue, provider, cycle, session);
|
|
3449
3471
|
return { success: false, blocked: false, continueRequested: true, code: lastCode, output: lastOutput, turns: turnIndex };
|
|
3450
3472
|
}
|
|
3473
|
+
logger.info({ issueId: issue.id, identifier: issue.identifier, turn: turnIndex }, "[Agent] Session completed successfully");
|
|
3451
3474
|
session.status = "done";
|
|
3452
3475
|
await persistAgentSessionState(sessionKey, issue, provider, cycle, session);
|
|
3453
3476
|
return { success: true, blocked: false, continueRequested: false, code: lastCode, output: lastOutput, turns: turnIndex };
|
|
@@ -3455,6 +3478,7 @@ Agent requested additional turns beyond configured limit (${maxTurns}).`, state.
|
|
|
3455
3478
|
async function runAgentPipeline(state, issue, workspacePath, basePromptText, basePromptFile, workflowDefinition, workflowConfig) {
|
|
3456
3479
|
const providers = getEffectiveAgentProviders(state, issue, workflowDefinition, workflowConfig);
|
|
3457
3480
|
const attempt = issue.attempts + 1;
|
|
3481
|
+
logger.debug({ issueId: issue.id, identifier: issue.identifier, attempt, providers: providers.map((p) => `${p.role}:${p.provider}`) }, "[Agent] Starting pipeline");
|
|
3458
3482
|
const { pipeline, key: pipelineFile } = await loadAgentPipelineState(issue, attempt, providers);
|
|
3459
3483
|
const activeProvider = providers[clamp(pipeline.activeIndex, 0, Math.max(0, providers.length - 1))];
|
|
3460
3484
|
const executorIndex = providers.findIndex((provider) => provider.role === "executor");
|
|
@@ -3527,6 +3551,7 @@ async function runIssueOnce(state, issue, running, workflowDefinition) {
|
|
|
3527
3551
|
const startTs = Date.now();
|
|
3528
3552
|
const isReview = issue.state === "In Review";
|
|
3529
3553
|
const isResuming = issue.state === "Running" || issue.state === "Interrupted";
|
|
3554
|
+
logger.info({ issueId: issue.id, identifier: issue.identifier, state: issue.state, isReview, isResuming, attempt: issue.attempts + 1, maxAttempts: issue.maxAttempts }, "[Agent] Starting issue execution");
|
|
3530
3555
|
running.add(issue.id);
|
|
3531
3556
|
state.metrics.activeWorkers += 1;
|
|
3532
3557
|
issue.startedAt = issue.startedAt ?? now();
|
|
@@ -3704,6 +3729,8 @@ async function runIssueOnce(state, issue, running, workflowDefinition) {
|
|
|
3704
3729
|
addEvent(state, issue.id, "error", `Issue ${issue.identifier} blocked after unexpected failure.`);
|
|
3705
3730
|
}
|
|
3706
3731
|
} finally {
|
|
3732
|
+
const elapsedMs = Date.now() - startTs;
|
|
3733
|
+
logger.info({ issueId: issue.id, identifier: issue.identifier, finalState: issue.state, elapsedMs, attempts: issue.attempts }, "[Agent] Issue execution finished");
|
|
3707
3734
|
issue.updatedAt = now();
|
|
3708
3735
|
markIssueDirty(issue.id);
|
|
3709
3736
|
state.metrics.activeWorkers = Math.max(state.metrics.activeWorkers - 1, 0);
|
|
@@ -4475,6 +4502,7 @@ async function ensureNotStale(state, staleTimeoutMs) {
|
|
|
4475
4502
|
const limit = Date.now() - staleTimeoutMs;
|
|
4476
4503
|
for (const issue of state.issues) {
|
|
4477
4504
|
if (EXECUTING_STATES.has(issue.state) && Date.parse(issue.updatedAt) < limit && !TERMINAL_STATES.has(issue.state) && !issueHasResumableSession(issue)) {
|
|
4505
|
+
logger.info({ issueId: issue.id, identifier: issue.identifier, state: issue.state, updatedAt: issue.updatedAt }, "[Scheduler] Recovering stale issue");
|
|
4478
4506
|
issue.attempts += 1;
|
|
4479
4507
|
issue.nextRetryAt = getNextRetryAt(issue, state.config.retryDelayMs);
|
|
4480
4508
|
issue.startedAt = void 0;
|
|
@@ -4493,7 +4521,11 @@ function isPerStateFull(issue, state, running) {
|
|
|
4493
4521
|
return count >= limit;
|
|
4494
4522
|
}
|
|
4495
4523
|
function pickNextIssues(state, running, workflowDefinition) {
|
|
4496
|
-
|
|
4524
|
+
const candidates = state.issues.filter((issue) => canRunIssue(issue, running, state) && !isPerStateFull(issue, state, running));
|
|
4525
|
+
if (candidates.length > 0) {
|
|
4526
|
+
logger.debug({ candidates: candidates.map((i) => ({ id: i.identifier, state: i.state, priority: i.priority })) }, "[Scheduler] Eligible candidates for dispatch");
|
|
4527
|
+
}
|
|
4528
|
+
return candidates.sort((a, b) => {
|
|
4497
4529
|
const stateWeight = (c) => c.state === "Running" ? 0 : c.state === "Blocked" ? 2 : 1;
|
|
4498
4530
|
const weightDiff = stateWeight(a) - stateWeight(b);
|
|
4499
4531
|
if (weightDiff !== 0) return weightDiff;
|
|
@@ -4545,9 +4577,12 @@ async function scheduler(state, running, runForever, workflowDefinition) {
|
|
|
4545
4577
|
} else {
|
|
4546
4578
|
const ready = pickNextIssues(state, running, workflowDefinition);
|
|
4547
4579
|
const slots = state.config.workerConcurrency - running.size;
|
|
4548
|
-
if (slots > 0) {
|
|
4580
|
+
if (slots > 0 && ready.length > 0) {
|
|
4549
4581
|
const next = ready.slice(0, Math.max(0, slots));
|
|
4582
|
+
logger.debug({ slots, readyCount: ready.length, dispatching: next.map((i) => i.identifier) }, "[Scheduler] Dispatching issues");
|
|
4550
4583
|
await Promise.all(next.map((issue) => runIssueOnce(state, issue, running, workflowDefinition)));
|
|
4584
|
+
} else if (ready.length > 0 && slots <= 0) {
|
|
4585
|
+
logger.debug({ runningCount: running.size, readyCount: ready.length, concurrency: state.config.workerConcurrency }, "[Scheduler] No slots available, waiting");
|
|
4551
4586
|
}
|
|
4552
4587
|
}
|
|
4553
4588
|
state.updatedAt = now();
|
|
@@ -4556,7 +4591,7 @@ async function scheduler(state, running, runForever, workflowDefinition) {
|
|
|
4556
4591
|
await persistState(state);
|
|
4557
4592
|
lastPersistAt = Date.now();
|
|
4558
4593
|
}
|
|
4559
|
-
logger.debug("Scheduler
|
|
4594
|
+
logger.debug({ runningCount: running.size, issueCount: state.issues.length, dirty: hasDirtyState() }, "[Scheduler] Tick completed");
|
|
4560
4595
|
const effectivePoll = running.size > 0 ? ACTIVE_POLL_MS : IDLE_POLL_MS;
|
|
4561
4596
|
await Promise.race([
|
|
4562
4597
|
sleep(effectivePoll),
|
|
@@ -4581,11 +4616,16 @@ async function scheduler(state, running, runForever, workflowDefinition) {
|
|
|
4581
4616
|
const next = ready.slice(0, Math.max(0, slots));
|
|
4582
4617
|
if (next.length === 0 && running.size === 0) {
|
|
4583
4618
|
if (state.issues.some((issue) => issue.state === "Blocked" && issue.nextRetryAt && issue.attempts < issue.maxAttempts)) {
|
|
4619
|
+
logger.debug("[Scheduler] Batch mode: waiting for blocked issues to become eligible for retry");
|
|
4584
4620
|
await sleep(state.config.pollIntervalMs);
|
|
4585
4621
|
continue;
|
|
4586
4622
|
}
|
|
4623
|
+
logger.debug("[Scheduler] Batch mode: no more work to do, exiting loop");
|
|
4587
4624
|
break;
|
|
4588
4625
|
}
|
|
4626
|
+
if (next.length > 0) {
|
|
4627
|
+
logger.debug({ slots, dispatching: next.map((i) => i.identifier) }, "[Scheduler] Batch mode: dispatching issues");
|
|
4628
|
+
}
|
|
4589
4629
|
await Promise.all(next.map((issue) => runIssueOnce(state, issue, running, workflowDefinition)));
|
|
4590
4630
|
state.updatedAt = now();
|
|
4591
4631
|
await persistState(state);
|
|
@@ -5054,6 +5094,7 @@ function extractPlanTokenUsage(raw) {
|
|
|
5054
5094
|
}
|
|
5055
5095
|
async function generatePlan(title, description, config, workflowDefinition, options) {
|
|
5056
5096
|
const fast = options?.fast ?? false;
|
|
5097
|
+
logger.info({ title: title.slice(0, 80), fast }, "[Planner] Starting plan generation");
|
|
5057
5098
|
const providers = detectAvailableProviders();
|
|
5058
5099
|
const available = providers.filter((p) => p.available).map((p) => p.name);
|
|
5059
5100
|
let planStageProvider;
|
|
@@ -5074,6 +5115,7 @@ async function generatePlan(title, description, config, workflowDefinition, opti
|
|
|
5074
5115
|
const effectiveEffort = fast ? "low" : planStageEffort || "medium";
|
|
5075
5116
|
const command = getPlanCommand(preferred, planStageModel);
|
|
5076
5117
|
if (!command) throw new Error(`No command configured for provider ${preferred}.`);
|
|
5118
|
+
logger.debug({ provider: preferred, model: planStageModel, effort: effectiveEffort, command: command.slice(0, 120) }, "[Planner] Provider selected for plan generation");
|
|
5077
5119
|
const planStartMs = Date.now();
|
|
5078
5120
|
const session = {
|
|
5079
5121
|
title,
|
|
@@ -5158,13 +5200,14 @@ async function generatePlan(title, description, config, workflowDefinition, opti
|
|
|
5158
5200
|
});
|
|
5159
5201
|
});
|
|
5160
5202
|
logger.info({ rawOutput: output.slice(0, 2e3) }, `Plan raw output from ${preferred}`);
|
|
5203
|
+
logger.debug({ outputLength: output.length }, "[Planner] Plan command completed, parsing output");
|
|
5161
5204
|
const plan = parsePlanOutput(output);
|
|
5162
5205
|
if (!plan) {
|
|
5163
5206
|
session.status = "error";
|
|
5164
5207
|
session.error = `Could not parse plan. Output: ${output.slice(0, 500)}`;
|
|
5165
5208
|
session.pid = null;
|
|
5166
5209
|
await persistSession(session);
|
|
5167
|
-
logger.error({ rawOutput: output.slice(0, 2e3) }, "Could not parse plan from AI output");
|
|
5210
|
+
logger.error({ rawOutput: output.slice(0, 2e3) }, "[Planner] Could not parse plan from AI output");
|
|
5168
5211
|
throw new Error(session.error);
|
|
5169
5212
|
}
|
|
5170
5213
|
plan.provider = planStageModel ? `${preferred}/${planStageModel}` : preferred;
|
|
@@ -6061,6 +6104,7 @@ function sendToAllClients(data) {
|
|
|
6061
6104
|
function broadcastToWebSocketClients(message) {
|
|
6062
6105
|
if (wsClients.size === 0) return;
|
|
6063
6106
|
broadcastSeq++;
|
|
6107
|
+
logger.debug({ seq: broadcastSeq, type: message.type, clientCount: wsClients.size }, "[WebSocket] Broadcasting state update");
|
|
6064
6108
|
const issues = message.issues;
|
|
6065
6109
|
if (issues && lastBroadcastIssueSnapshot.size > 0) {
|
|
6066
6110
|
const currentIds = /* @__PURE__ */ new Set();
|
|
@@ -6108,6 +6152,7 @@ function broadcastToWebSocketClients(message) {
|
|
|
6108
6152
|
}));
|
|
6109
6153
|
}
|
|
6110
6154
|
async function startApiServer(state, port, workflowDefinition) {
|
|
6155
|
+
logger.info({ port }, "[API] Starting API server");
|
|
6111
6156
|
const stateDb2 = getStateDb();
|
|
6112
6157
|
if (!stateDb2) {
|
|
6113
6158
|
throw new Error("Cannot start API plugin before the database is initialized.");
|
|
@@ -6455,6 +6500,7 @@ async function startApiServer(state, port, workflowDefinition) {
|
|
|
6455
6500
|
const title = toStringValue(payload.title);
|
|
6456
6501
|
const description = toStringValue(payload.description);
|
|
6457
6502
|
if (!title) return c.json({ ok: false, error: "Title is required." }, 400);
|
|
6503
|
+
logger.info({ title: title.slice(0, 80) }, "[API] POST /api/planning/generate");
|
|
6458
6504
|
const result = await generatePlan(title, description, state.config, workflowDefinition);
|
|
6459
6505
|
return c.json({ ok: true, plan: result.plan, usage: result.usage });
|
|
6460
6506
|
} catch (error) {
|
|
@@ -6482,6 +6528,7 @@ async function startApiServer(state, port, workflowDefinition) {
|
|
|
6482
6528
|
"POST /api/issues/create": async (c) => {
|
|
6483
6529
|
try {
|
|
6484
6530
|
const payload = await c.req.json();
|
|
6531
|
+
logger.info({ title: toStringValue(payload.title, "").slice(0, 80) }, "[API] POST /api/issues/create");
|
|
6485
6532
|
const issue = createIssueFromPayload(payload, state.issues, workflowDefinition);
|
|
6486
6533
|
state.issues.push(issue);
|
|
6487
6534
|
markIssueDirty(issue.id);
|
|
@@ -6531,6 +6578,7 @@ async function startApiServer(state, port, workflowDefinition) {
|
|
|
6531
6578
|
}
|
|
6532
6579
|
try {
|
|
6533
6580
|
const payload = await c.req.json();
|
|
6581
|
+
logger.info({ issueId, identifier: issue.identifier, targetState: payload.state }, "[API] POST /api/issues/:id/state");
|
|
6534
6582
|
await handleStatePatch(state, issue, payload);
|
|
6535
6583
|
await persistState(state);
|
|
6536
6584
|
wakeScheduler();
|
|
@@ -6540,6 +6588,7 @@ async function startApiServer(state, port, workflowDefinition) {
|
|
|
6540
6588
|
}
|
|
6541
6589
|
},
|
|
6542
6590
|
"POST /api/issues/:id/retry": async (c) => {
|
|
6591
|
+
logger.info({ issueId: parseIssue(c) }, "[API] POST /api/issues/:id/retry");
|
|
6543
6592
|
return mutateIssueState(c, async (issue) => {
|
|
6544
6593
|
if (TERMINAL_STATES.has(issue.state)) {
|
|
6545
6594
|
await transitionIssueState(issue, "Todo", "Manual retry requested.");
|
|
@@ -6552,6 +6601,7 @@ async function startApiServer(state, port, workflowDefinition) {
|
|
|
6552
6601
|
});
|
|
6553
6602
|
},
|
|
6554
6603
|
"POST /api/issues/:id/cancel": async (c) => {
|
|
6604
|
+
logger.info({ issueId: parseIssue(c) }, "[API] POST /api/issues/:id/cancel");
|
|
6555
6605
|
return mutateIssueState(c, async (issue) => {
|
|
6556
6606
|
await transitionIssueState(issue, "Cancelled", "Manual cancel requested.");
|
|
6557
6607
|
addEvent(state, issue.id, "manual", `Manual cancel requested for ${issue.id}.`);
|
|
@@ -6577,6 +6627,7 @@ async function startApiServer(state, port, workflowDefinition) {
|
|
|
6577
6627
|
});
|
|
6578
6628
|
},
|
|
6579
6629
|
"POST /api/issues/:id/approve": async (c) => {
|
|
6630
|
+
logger.info({ issueId: parseIssue(c) }, "[API] POST /api/issues/:id/approve");
|
|
6580
6631
|
return mutateIssueState(c, async (issue) => {
|
|
6581
6632
|
if (issue.state !== "Planning") {
|
|
6582
6633
|
throw new Error(`Cannot approve issue in state ${issue.state}. Must be in Planning.`);
|
|
@@ -6586,6 +6637,7 @@ async function startApiServer(state, port, workflowDefinition) {
|
|
|
6586
6637
|
});
|
|
6587
6638
|
},
|
|
6588
6639
|
"POST /api/issues/:id/merge": async (c) => {
|
|
6640
|
+
logger.info({ issueId: parseIssue(c) }, "[API] POST /api/issues/:id/merge");
|
|
6589
6641
|
try {
|
|
6590
6642
|
const issueId = parseIssue(c);
|
|
6591
6643
|
if (!issueId) return c.json({ ok: false, error: "Issue id is required." }, 400);
|
|
@@ -7074,7 +7126,11 @@ function isStateNotFoundError(error) {
|
|
|
7074
7126
|
return false;
|
|
7075
7127
|
}
|
|
7076
7128
|
async function loadPersistedState() {
|
|
7077
|
-
if (!runtimeStateResource)
|
|
7129
|
+
if (!runtimeStateResource) {
|
|
7130
|
+
logger.debug("[Store] No runtime state resource available, skipping load");
|
|
7131
|
+
return null;
|
|
7132
|
+
}
|
|
7133
|
+
logger.debug("[Store] Loading persisted state from s3db");
|
|
7078
7134
|
try {
|
|
7079
7135
|
const record2 = await runtimeStateResource.get(S3DB_RUNTIME_RECORD_ID);
|
|
7080
7136
|
if (record2?.state && typeof record2.state === "object") {
|
|
@@ -7124,6 +7180,11 @@ async function persistState(state) {
|
|
|
7124
7180
|
};
|
|
7125
7181
|
if (!runtimeStateResource) return;
|
|
7126
7182
|
const dirty = hasDirtyState();
|
|
7183
|
+
const dirtyIssueCount = getDirtyIssueIds().size;
|
|
7184
|
+
const dirtyEventCount = getDirtyEventIds().size;
|
|
7185
|
+
if (dirty || dirtyIssueCount > 0 || dirtyEventCount > 0) {
|
|
7186
|
+
logger.debug({ dirty, dirtyIssues: dirtyIssueCount, dirtyEvents: dirtyEventCount }, "[Store] Persisting state");
|
|
7187
|
+
}
|
|
7127
7188
|
if (dirty) {
|
|
7128
7189
|
await runtimeStateResource.replace(S3DB_RUNTIME_RECORD_ID, {
|
|
7129
7190
|
id: S3DB_RUNTIME_RECORD_ID,
|
|
@@ -7195,6 +7256,7 @@ async function replacePersistedSetting(setting) {
|
|
|
7195
7256
|
await settingStateResource.replace(setting.id, setting);
|
|
7196
7257
|
}
|
|
7197
7258
|
async function closeStateStore() {
|
|
7259
|
+
logger.info("[Store] Closing state store and plugins");
|
|
7198
7260
|
clearApiRuntimeContext();
|
|
7199
7261
|
if (activeEcPlugin?.stop) {
|
|
7200
7262
|
try {
|
|
@@ -7320,6 +7382,8 @@ async function main() {
|
|
|
7320
7382
|
}
|
|
7321
7383
|
mkdirSync5(STATE_ROOT, { recursive: true });
|
|
7322
7384
|
initLogger(STATE_ROOT);
|
|
7385
|
+
logger.info("[Boot] Fifony runtime starting");
|
|
7386
|
+
logger.info({ stateRoot: STATE_ROOT, cwd: process.cwd() }, "[Boot] State root initialized");
|
|
7323
7387
|
const detectedProviders = detectAvailableProviders();
|
|
7324
7388
|
for (const p of detectedProviders) {
|
|
7325
7389
|
logger.info(`Provider ${p.name}: ${p.available ? `available at ${p.path}` : "not found"}`);
|
|
@@ -7331,7 +7395,9 @@ async function main() {
|
|
|
7331
7395
|
const skipSource = fastBoot || args.includes("--skip-source");
|
|
7332
7396
|
if (skipSource) setSkipSource(true);
|
|
7333
7397
|
debugBoot("main:state-root-ready");
|
|
7398
|
+
logger.debug("[Boot] Loading workflow definition");
|
|
7334
7399
|
const workflowDefinition = loadWorkflowDefinition();
|
|
7400
|
+
logger.info({ workflowPath: workflowDefinition.workflowPath }, "[Boot] Workflow definition loaded");
|
|
7335
7401
|
debugBoot("main:workflow-loaded");
|
|
7336
7402
|
const port = parsePort(args);
|
|
7337
7403
|
let config = applyWorkflowConfig(deriveConfig(args), workflowDefinition, port);
|
|
@@ -7348,7 +7414,9 @@ async function main() {
|
|
|
7348
7414
|
const dashboardPort = port ?? (config.dashboardPort ? Number.parseInt(config.dashboardPort, 10) : void 0);
|
|
7349
7415
|
const skipRecovery = args.includes("--skip-recovery") || args.includes("--fast-boot");
|
|
7350
7416
|
debugBoot("main:phase-b-start");
|
|
7417
|
+
logger.debug("[Boot] Initializing state store (s3db)");
|
|
7351
7418
|
await initStateStore();
|
|
7419
|
+
logger.info("[Boot] State store initialized");
|
|
7352
7420
|
debugBoot("main:store-initialized");
|
|
7353
7421
|
const earlyState = {
|
|
7354
7422
|
startedAt: now(),
|
|
@@ -7374,12 +7442,14 @@ async function main() {
|
|
|
7374
7442
|
}
|
|
7375
7443
|
}
|
|
7376
7444
|
debugBoot("main:phase-c-start");
|
|
7445
|
+
logger.debug("[Boot] Loading persisted state, settings, and recovering sessions");
|
|
7377
7446
|
const [previous, persistedSettings] = await Promise.all([
|
|
7378
7447
|
loadPersistedState(),
|
|
7379
7448
|
loadRuntimeSettings(),
|
|
7380
7449
|
persistDetectedProvidersSetting(detectedProviders),
|
|
7381
7450
|
recoverPlanningSession()
|
|
7382
7451
|
]);
|
|
7452
|
+
logger.info({ hadPreviousState: previous !== null, issueCount: previous?.issues?.length ?? 0, settingsCount: persistedSettings.length }, "[Boot] State loaded from persistence");
|
|
7383
7453
|
debugBoot("main:state-loaded");
|
|
7384
7454
|
config = applyPersistedSettings(config, persistedSettings);
|
|
7385
7455
|
await syncRuntimeConfigSettings(config, persistedSettings);
|
|
@@ -7419,6 +7489,7 @@ async function main() {
|
|
|
7419
7489
|
});
|
|
7420
7490
|
}
|
|
7421
7491
|
if (!skipRecovery) {
|
|
7492
|
+
logger.debug({ issueCount: state.issues.filter((i) => i.state === "Running" || i.state === "Interrupted" || i.state === "Queued").length }, "[Boot] Checking for orphaned agent processes");
|
|
7422
7493
|
for (const issue of state.issues) {
|
|
7423
7494
|
if (issue.state === "Running" || issue.state === "Interrupted" || issue.state === "Queued") {
|
|
7424
7495
|
const { alive, pid } = isAgentStillRunning(issue);
|
|
@@ -7456,6 +7527,7 @@ async function main() {
|
|
|
7456
7527
|
try {
|
|
7457
7528
|
addEvent(state, void 0, "info", `Runtime started in local-only mode (filesystem tracker).`);
|
|
7458
7529
|
const runForever = !runOnce && (Boolean(dashboardPort) || interfaceMode === "mcp");
|
|
7530
|
+
logger.info({ runForever, runOnce, dashboardPort, interfaceMode }, "[Boot] Entering scheduler loop");
|
|
7459
7531
|
await scheduler(state, running, runForever, workflowDefinition);
|
|
7460
7532
|
} catch (error) {
|
|
7461
7533
|
console.error("FATAL STACK TRACE:", error);
|