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 = "1773754620530";
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) return false;
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) return false;
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)) return false;
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
- return state.issues.filter((issue) => canRunIssue(issue, running, state) && !isPerStateFull(issue, state, running)).sort((a, b) => {
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 tick completed.");
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) return null;
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);