@bojackduy/opencode-loopd 1.6.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -147,7 +147,7 @@ import { randomUUID } from "crypto";
147
147
  import { promises as fs } from "fs";
148
148
  import path from "path";
149
149
  import os from "os";
150
- var CURRENT_VERSION = 5;
150
+ var CURRENT_VERSION = 6;
151
151
  function emptyState() {
152
152
  return { version: CURRENT_VERSION, revision: 0, goals: [], runtimes: [], commandLedger: [] };
153
153
  }
@@ -310,6 +310,32 @@ function migrate(state) {
310
310
  workerUnreachableNotifiedAt: rt.workerUnreachableNotifiedAt ?? undefined
311
311
  }));
312
312
  }
313
+ if (result.version < 6) {
314
+ result.version = 6;
315
+ result.goals = result.goals.map((goal) => ({
316
+ ...goal,
317
+ config: {
318
+ ...goal.config,
319
+ schedule: goal.config?.schedule ?? undefined
320
+ }
321
+ }));
322
+ result.goals = result.goals.map((goal) => {
323
+ const s = goal.config?.schedule;
324
+ if (s && typeof s.everyMs === "number" && s.everyMs >= 1000)
325
+ return goal;
326
+ if (s) {
327
+ const { schedule: _s, ...restConfig } = goal.config;
328
+ return { ...goal, config: restConfig };
329
+ }
330
+ return goal;
331
+ });
332
+ result.runtimes = result.runtimes.map((rt) => ({
333
+ ...rt,
334
+ scheduleRunCount: typeof rt.scheduleRunCount === "number" ? rt.scheduleRunCount : 0,
335
+ nextRunAt: rt.nextRunAt ?? undefined,
336
+ lastScheduleAt: rt.lastScheduleAt ?? undefined
337
+ }));
338
+ }
313
339
  return result;
314
340
  }
315
341
  async function writeAtomic(target, contents) {
@@ -470,14 +496,7 @@ function resolveGoalCreationConfig(input) {
470
496
  const defaults = input.defaults || {};
471
497
  const explicitAgent = cleanText(requested.agent);
472
498
  const defaultAgent = cleanText(defaults.defaultAgent);
473
- const agent = explicitAgent || defaultAgent;
474
- if (!agent) {
475
- return {
476
- ok: false,
477
- errorCode: "missing_agent",
478
- message: "An agent is required. Pass agent explicitly or configure plugin option defaultAgent."
479
- };
480
- }
499
+ const agent = explicitAgent || defaultAgent || undefined;
481
500
  const workspaceWrite = requested.workspaceWrite ?? true;
482
501
  const explicitChecks = cleanList(requested.checks);
483
502
  const defaultChecks = workspaceWrite ? cleanList(defaults.defaultChecks) : [];
@@ -1799,7 +1818,13 @@ function createGoalService(host) {
1799
1818
  const state1 = await mutateState(directory, `goal.create:${id}`, async (state) => {
1800
1819
  assertWorkspaceWriteAvailable(state, goal);
1801
1820
  state.goals.push(goal);
1802
- state.runtimes.push(createRuntimeState(id));
1821
+ const rt = createRuntimeState(id);
1822
+ if (goal.config.schedule) {
1823
+ rt.scheduleRunCount = 0;
1824
+ rt.nextRunAt = undefined;
1825
+ rt.lastScheduleAt = undefined;
1826
+ }
1827
+ state.runtimes.push(rt);
1803
1828
  const runtime2 = state.runtimes.find((r) => r.goalID === id);
1804
1829
  if (runtime2)
1805
1830
  runtime2.phase = "queued";
@@ -2281,6 +2306,119 @@ function createGoalService(host) {
2281
2306
  return { start, continueTurn, nudge, pause, resume, retry, clear, getWorker, getActiveWorkers, reconcile };
2282
2307
  }
2283
2308
 
2309
+ // src/application/schedule-worker.ts
2310
+ import { randomUUID as randomUUID4 } from "crypto";
2311
+ function createScheduleWorker(options) {
2312
+ const { directory, goalService } = options;
2313
+ const intervalMs = options.intervalMs ?? 5000;
2314
+ let running = false;
2315
+ let timer;
2316
+ function start() {
2317
+ if (running)
2318
+ return;
2319
+ running = true;
2320
+ timer = setInterval(() => {
2321
+ tick().catch(() => {});
2322
+ }, intervalMs);
2323
+ }
2324
+ function stop() {
2325
+ running = false;
2326
+ if (timer)
2327
+ clearInterval(timer);
2328
+ timer = undefined;
2329
+ }
2330
+ function isRunning() {
2331
+ return running;
2332
+ }
2333
+ async function tick() {
2334
+ const state = await readState(directory);
2335
+ let resurrected = 0;
2336
+ for (const goal of state.goals) {
2337
+ const schedule = goal.config.schedule;
2338
+ if (!schedule || typeof schedule.everyMs !== "number" || schedule.everyMs < 1000)
2339
+ continue;
2340
+ const runtime = state.runtimes.find((r) => r.goalID === goal.id);
2341
+ if (!runtime)
2342
+ continue;
2343
+ if (goal.status !== "complete")
2344
+ continue;
2345
+ const count = runtime.scheduleRunCount ?? 0;
2346
+ const max = schedule.maxRuns;
2347
+ if (typeof max === "number" && count >= max)
2348
+ continue;
2349
+ const nextAt = runtime.nextRunAt;
2350
+ if (!nextAt)
2351
+ continue;
2352
+ if (Date.now() < Date.parse(nextAt))
2353
+ continue;
2354
+ const activeWriter = state.goals.find((g) => g.id !== goal.id && g.status === "active" && g.config.workspaceWrite);
2355
+ if (goal.config.workspaceWrite && activeWriter) {
2356
+ await logServerEvent(directory, "schedule.skipped-writer-active", {
2357
+ goalID: goal.id,
2358
+ activeWriter: activeWriter.id
2359
+ });
2360
+ continue;
2361
+ }
2362
+ if (runtime.phase === "running" || runtime.phase === "queued" || runtime.phase === "compacting")
2363
+ continue;
2364
+ if (leaseIsValid(runtime))
2365
+ continue;
2366
+ const didResurrect = await mutateState(directory, `schedule.tick:${goal.id}`, async (s) => {
2367
+ const g = s.goals.find((x) => x.id === goal.id);
2368
+ const rt = s.runtimes.find((x) => x.goalID === goal.id);
2369
+ if (!g || !rt)
2370
+ return s;
2371
+ if (g.status !== "complete")
2372
+ return s;
2373
+ const curCount = rt.scheduleRunCount ?? 0;
2374
+ if (typeof max === "number" && curCount >= max)
2375
+ return s;
2376
+ const curNext = rt.nextRunAt;
2377
+ if (!curNext || Date.now() < Date.parse(curNext))
2378
+ return s;
2379
+ g.status = "active";
2380
+ g.updatedAt = new Date().toISOString();
2381
+ g.blocker = undefined;
2382
+ rt.phase = "idle";
2383
+ rt.consecutiveFailures = 0;
2384
+ rt.noProgressCount = 0;
2385
+ rt.progressDuringTurn = false;
2386
+ rt.forceFinishRequested = undefined;
2387
+ rt.lastError = undefined;
2388
+ rt.lastScheduleAt = new Date().toISOString();
2389
+ rt.updatedAt = new Date().toISOString();
2390
+ return s;
2391
+ });
2392
+ const after = didResurrect.goals.find((g) => g.id === goal.id);
2393
+ if (!after || after.status !== "active")
2394
+ continue;
2395
+ await appendEvent(directory, {
2396
+ version: 1,
2397
+ eventID: randomUUID4(),
2398
+ goalID: goal.id,
2399
+ type: "schedule.tick",
2400
+ scheduleRunCount: count,
2401
+ nextRunAt: nextAt,
2402
+ timestamp: new Date().toISOString(),
2403
+ revision: didResurrect.revision
2404
+ });
2405
+ await logServerEvent(directory, "schedule.resurrected", {
2406
+ goalID: goal.id,
2407
+ scheduleRunCount: count,
2408
+ nextRunAt: nextAt
2409
+ });
2410
+ const maxLabel = typeof max === "number" ? `/${max}` : "";
2411
+ await appendGoalInbox(directory, goal.id, "user", `Scheduled tick ${count + 1}${maxLabel} \u2014 re-execute the objective now. Previous completion: ${runtime.scheduleRunCount ?? 0} runs. Ensure artifact checks pass for this tick (e.g., append timestamp to tick.txt).`);
2412
+ try {
2413
+ await goalService.continueTurn(directory, goal.id);
2414
+ resurrected++;
2415
+ } catch {}
2416
+ }
2417
+ return resurrected;
2418
+ }
2419
+ return { start, stop, isRunning, tick };
2420
+ }
2421
+
2284
2422
  // src/server/host-adapter.ts
2285
2423
  var recentParentNotifies = new Map;
2286
2424
  function shouldDedupParentNotify(ownerSessionID, message) {
@@ -2431,7 +2569,7 @@ async function withTimeout(promise, timeoutMs, operation) {
2431
2569
  }
2432
2570
 
2433
2571
  // src/server/goal-tools.ts
2434
- import { randomUUID as randomUUID4 } from "crypto";
2572
+ import { randomUUID as randomUUID5 } from "crypto";
2435
2573
  import { tool } from "@opencode-ai/plugin/tool";
2436
2574
  // src/domain/verification.ts
2437
2575
  var MAX_RECENT_ATTEMPTS = 10;
@@ -2450,11 +2588,11 @@ var execAsync = promisify(execChild);
2450
2588
  function goalTools(dir, goalService, hostSessionID, defaults = {}) {
2451
2589
  return {
2452
2590
  loopd_create_goal: tool({
2453
- description: "Create a new background loop goal (contract: objective + checks + agent + workspaceWrite). " + "The engine spawns a dedicated worker session that does the work autonomously \u2014 it never runs in this chat. " + "Call this after clarifying the contract with the user. " + "Host is the acceptance authority: checks must pass for complete_goal (free retry if rejected <3, blocked after 3). " + "Workspace-writing goals are serialized (only one active writer) and require checks. " + "Specify 'agent' or configure plugin defaultAgent.",
2591
+ description: "Create a new background loop goal (contract: objective + checks + agent + workspaceWrite). " + "The engine spawns a dedicated worker session that does the work autonomously \u2014 it never runs in this chat. " + "Call this after clarifying the contract with the user. " + "Host is the acceptance authority: checks must pass for complete_goal (free retry if rejected <3, blocked after 3). " + "Workspace-writing goals are serialized (only one active writer) and require checks. " + "agent is optional \u2014 uses the parent session's agent if omitted, or configure plugin defaultAgent in opencode.jsonc.",
2454
2592
  args: {
2455
2593
  name: tool.schema.string().describe("Short goal name (used in the dashboard)."),
2456
2594
  objective: tool.schema.string().describe("What the goal should accomplish, in detail."),
2457
- agent: tool.schema.string().optional().describe("Agent to run the worker as. Required unless the plugin has defaultAgent configured."),
2595
+ agent: tool.schema.string().optional().describe("Agent to run the worker as. Optional \u2014 uses parent session's agent if omitted, or configure plugin defaultAgent in opencode.jsonc."),
2458
2596
  checks: tool.schema.array(tool.schema.string()).optional().describe('Shell commands that must pass for completion to be accepted. E.g. ["npm test"].'),
2459
2597
  checkCwd: tool.schema.string().optional().describe("Directory where completion checks run. Workspace-writing goals default to the project root."),
2460
2598
  workspaceWrite: tool.schema.boolean().optional().describe("Whether this goal edits the shared project workspace. Defaults to true; explicitly set false for artifact-only/read-only work."),
@@ -2463,7 +2601,9 @@ function goalTools(dir, goalService, hostSessionID, defaults = {}) {
2463
2601
  maxNoProgress: tool.schema.number().optional().describe("Block after N turns without progress."),
2464
2602
  maxFailures: tool.schema.number().optional().describe("Block after N consecutive failures."),
2465
2603
  compactEvery: tool.schema.number().optional().describe("Compact the worker session every N turns."),
2466
- timeoutMs: tool.schema.number().optional().describe("Per-turn timeout in ms.")
2604
+ timeoutMs: tool.schema.number().optional().describe("Per-turn timeout in ms."),
2605
+ scheduleEveryMs: tool.schema.number().optional().describe("Interval in ms to auto-requeue the same goal after each completion. Minimum 1000. Enables repetitive dialogue reduction."),
2606
+ scheduleMaxRuns: tool.schema.number().optional().describe("Maximum total runs including the initial run. Undefined = unlimited. Requires scheduleEveryMs.")
2467
2607
  },
2468
2608
  execute: async (args, context) => {
2469
2609
  const sessionID = context?.sessionID || hostSessionID;
@@ -2499,6 +2639,28 @@ function goalTools(dir, goalService, hostSessionID, defaults = {}) {
2499
2639
  config.compactEvery = args.compactEvery;
2500
2640
  if (args.timeoutMs !== undefined)
2501
2641
  config.timeoutMs = args.timeoutMs;
2642
+ if (args.scheduleEveryMs !== undefined) {
2643
+ const everyMs = args.scheduleEveryMs;
2644
+ if (typeof everyMs !== "number" || !Number.isFinite(everyMs) || everyMs < 1000) {
2645
+ return {
2646
+ title: "Goal not created",
2647
+ output: JSON.stringify({ ok: false, message: "scheduleEveryMs must be a number >= 1000", errorCode: "invalid_schedule" })
2648
+ };
2649
+ }
2650
+ const maxRuns = args.scheduleMaxRuns;
2651
+ if (maxRuns !== undefined && (typeof maxRuns !== "number" || !Number.isFinite(maxRuns) || maxRuns < 1 || Math.floor(maxRuns) !== maxRuns)) {
2652
+ return {
2653
+ title: "Goal not created",
2654
+ output: JSON.stringify({ ok: false, message: "scheduleMaxRuns must be an integer >= 1", errorCode: "invalid_schedule" })
2655
+ };
2656
+ }
2657
+ config.schedule = { everyMs, ...maxRuns !== undefined ? { maxRuns } : {} };
2658
+ } else if (args.scheduleMaxRuns !== undefined) {
2659
+ return {
2660
+ title: "Goal not created",
2661
+ output: JSON.stringify({ ok: false, message: "scheduleMaxRuns requires scheduleEveryMs", errorCode: "invalid_schedule" })
2662
+ };
2663
+ }
2502
2664
  const resolution = resolveGoalCreationConfig({
2503
2665
  directory: dir,
2504
2666
  objective: args.objective,
@@ -2604,7 +2766,7 @@ function goalTools(dir, goalService, hostSessionID, defaults = {}) {
2604
2766
  await writeState(dir, state);
2605
2767
  const event = {
2606
2768
  version: 1,
2607
- eventID: randomUUID4(),
2769
+ eventID: randomUUID5(),
2608
2770
  goalID: goal.id,
2609
2771
  type: "goal.progress",
2610
2772
  summary: args.summary,
@@ -2662,7 +2824,7 @@ Exit code: ${f.exitCode}${stdoutSnippet}${stderrSnippet}`;
2662
2824
  Working directory: ${cwd}
2663
2825
 
2664
2826
  ${failureDetails}`;
2665
- const attemptID = randomUUID4();
2827
+ const attemptID = randomUUID5();
2666
2828
  const verificationAttempt = {
2667
2829
  id: attemptID,
2668
2830
  sequence: runtime2.evaluatorRejectionCount,
@@ -2684,7 +2846,7 @@ ${failureDetails}`;
2684
2846
  runtime2.recentVerificationAttempts = appendVerificationAttempt(runtime2.recentVerificationAttempts || [], verificationAttempt);
2685
2847
  const rejectEvent = {
2686
2848
  version: 1,
2687
- eventID: randomUUID4(),
2849
+ eventID: randomUUID5(),
2688
2850
  goalID: goal.id,
2689
2851
  type: "goal.completion_rejected",
2690
2852
  attemptID,
@@ -2708,7 +2870,7 @@ ${failureDetails.slice(0, 500)}`,
2708
2870
  runtime2.forceFinishRequested = undefined;
2709
2871
  await appendEvent(dir, {
2710
2872
  version: 1,
2711
- eventID: randomUUID4(),
2873
+ eventID: randomUUID5(),
2712
2874
  goalID: goal.id,
2713
2875
  type: "goal.blocked",
2714
2876
  reason: goal.blocker.reason,
@@ -2745,8 +2907,33 @@ ${failureDetails.slice(0, 500)}`,
2745
2907
  const runtime = state.runtimes.find((r) => r.goalID === goal.id);
2746
2908
  if (runtime) {
2747
2909
  runtime.phase = "idle";
2910
+ runtime.leaseExpiresAt = undefined;
2911
+ runtime.turnStartedAt = undefined;
2912
+ runtime.activeRunID = undefined;
2913
+ runtime.activePromptMessageID = undefined;
2914
+ runtime.activePromptObservedAt = undefined;
2915
+ runtime.activeAssistantMessageID = undefined;
2916
+ runtime.activeAssistantCompletedAt = undefined;
2917
+ runtime.idleCandidateAt = undefined;
2918
+ runtime.idleCandidateGeneration = undefined;
2919
+ runtime.activeToolCallIDs = [];
2748
2920
  runtime.lastError = undefined;
2749
- const attemptID = randomUUID4();
2921
+ const schedule = goal.config.schedule;
2922
+ if (schedule && typeof schedule.everyMs === "number" && schedule.everyMs >= 1000) {
2923
+ const cur = typeof runtime.scheduleRunCount === "number" ? runtime.scheduleRunCount : 0;
2924
+ const nextCount = cur + 1;
2925
+ runtime.scheduleRunCount = nextCount;
2926
+ const max = schedule.maxRuns;
2927
+ const hasMore = typeof max === "number" ? nextCount < max : true;
2928
+ if (hasMore) {
2929
+ runtime.nextRunAt = new Date(Date.now() + schedule.everyMs).toISOString();
2930
+ runtime.lastScheduleAt = new Date().toISOString();
2931
+ } else {
2932
+ runtime.nextRunAt = undefined;
2933
+ }
2934
+ runtime.updatedAt = new Date().toISOString();
2935
+ }
2936
+ const attemptID = randomUUID5();
2750
2937
  const cwd = goal.config.checkCwd || goal.config.artifactDir || dir;
2751
2938
  const checks = (goal.config.checks || []).map((cmd) => ({
2752
2939
  command: cmd,
@@ -2770,7 +2957,7 @@ ${failureDetails.slice(0, 500)}`,
2770
2957
  await writeState(dir, state);
2771
2958
  const event = {
2772
2959
  version: 1,
2773
- eventID: randomUUID4(),
2960
+ eventID: randomUUID5(),
2774
2961
  goalID: goal.id,
2775
2962
  type: "goal.completed",
2776
2963
  summary: args.summary,
@@ -2822,7 +3009,7 @@ ${failureDetails.slice(0, 500)}`,
2822
3009
  await writeState(dir, state);
2823
3010
  const event = {
2824
3011
  version: 1,
2825
- eventID: randomUUID4(),
3012
+ eventID: randomUUID5(),
2826
3013
  goalID: goal.id,
2827
3014
  type: "goal.blocked",
2828
3015
  reason: args.reason,
@@ -2870,7 +3057,8 @@ function formatGoalStructured(goal, runtime) {
2870
3057
  maxNoProgress: goal.config.maxNoProgress,
2871
3058
  maxFailures: goal.config.maxFailures,
2872
3059
  compactEvery: goal.config.compactEvery,
2873
- timeoutMs: goal.config.timeoutMs
3060
+ timeoutMs: goal.config.timeoutMs,
3061
+ schedule: goal.config.schedule
2874
3062
  },
2875
3063
  lastProgress: goal.lastProgress,
2876
3064
  completionEvidence: goal.completionEvidence,
@@ -2902,7 +3090,10 @@ function formatGoalStructured(goal, runtime) {
2902
3090
  lastUnknownStatusAt: runtime.lastUnknownStatusAt,
2903
3091
  workerUnreachableNotifiedAt: runtime.workerUnreachableNotifiedAt,
2904
3092
  lastVerificationAttempt: runtime.lastVerificationAttempt,
2905
- recentVerificationAttempts: runtime.recentVerificationAttempts
3093
+ recentVerificationAttempts: runtime.recentVerificationAttempts,
3094
+ scheduleRunCount: runtime.scheduleRunCount,
3095
+ nextRunAt: runtime.nextRunAt,
3096
+ lastScheduleAt: runtime.lastScheduleAt
2906
3097
  };
2907
3098
  }
2908
3099
  return JSON.stringify(output, null, 2);
@@ -3008,7 +3199,8 @@ function ownerTools(options) {
3008
3199
  checks: goal.config.checks,
3009
3200
  checkCwd: goal.config.checkCwd,
3010
3201
  workspaceWrite: goal.config.workspaceWrite,
3011
- agent: goal.config.agent
3202
+ agent: goal.config.agent,
3203
+ schedule: goal.config.schedule
3012
3204
  },
3013
3205
  lastProgress: goal.lastProgress,
3014
3206
  completionEvidence: goal.completionEvidence,
@@ -3025,7 +3217,10 @@ function ownerTools(options) {
3025
3217
  consecutiveFailures: runtime.consecutiveFailures,
3026
3218
  lastError: runtime.lastError,
3027
3219
  lastProgressAt: runtime.lastProgressAt,
3028
- lastRunAt: runtime.lastRunAt
3220
+ lastRunAt: runtime.lastRunAt,
3221
+ scheduleRunCount: runtime.scheduleRunCount,
3222
+ nextRunAt: runtime.nextRunAt,
3223
+ lastScheduleAt: runtime.lastScheduleAt
3029
3224
  } : undefined
3030
3225
  }, null, 2)
3031
3226
  };
@@ -3294,6 +3489,11 @@ var server = async ({ client, directory }, pluginOptions) => {
3294
3489
  goalService,
3295
3490
  pollIntervalMs: 30000
3296
3491
  });
3492
+ const scheduleWorker = createScheduleWorker({
3493
+ directory,
3494
+ goalService,
3495
+ intervalMs: 5000
3496
+ });
3297
3497
  let started = false;
3298
3498
  let reconciliationStarted = false;
3299
3499
  function ensureStarted() {
@@ -3302,6 +3502,7 @@ var server = async ({ client, directory }, pluginOptions) => {
3302
3502
  started = true;
3303
3503
  engine.start();
3304
3504
  worker.start();
3505
+ scheduleWorker.start();
3305
3506
  }
3306
3507
  function reconcileInBackground() {
3307
3508
  if (reconciliationStarted)
@@ -3400,6 +3601,7 @@ var server = async ({ client, directory }, pluginOptions) => {
3400
3601
  dispose: async () => {
3401
3602
  engine.stop();
3402
3603
  await worker.stop();
3604
+ scheduleWorker.stop();
3403
3605
  }
3404
3606
  };
3405
3607
  };
package/dist/tui.js CHANGED
@@ -22,7 +22,7 @@ import { useKeyboard } from "@opentui/solid";
22
22
  // src/infrastructure/state-repository.ts
23
23
  import { promises as fs } from "fs";
24
24
  import path from "path";
25
- var CURRENT_VERSION = 5;
25
+ var CURRENT_VERSION = 6;
26
26
  function emptyState() {
27
27
  return { version: CURRENT_VERSION, revision: 0, goals: [], runtimes: [], commandLedger: [] };
28
28
  }
@@ -123,6 +123,32 @@ function migrate(state) {
123
123
  workerUnreachableNotifiedAt: rt.workerUnreachableNotifiedAt ?? undefined
124
124
  }));
125
125
  }
126
+ if (result.version < 6) {
127
+ result.version = 6;
128
+ result.goals = result.goals.map((goal) => ({
129
+ ...goal,
130
+ config: {
131
+ ...goal.config,
132
+ schedule: goal.config?.schedule ?? undefined
133
+ }
134
+ }));
135
+ result.goals = result.goals.map((goal) => {
136
+ const s = goal.config?.schedule;
137
+ if (s && typeof s.everyMs === "number" && s.everyMs >= 1000)
138
+ return goal;
139
+ if (s) {
140
+ const { schedule: _s, ...restConfig } = goal.config;
141
+ return { ...goal, config: restConfig };
142
+ }
143
+ return goal;
144
+ });
145
+ result.runtimes = result.runtimes.map((rt) => ({
146
+ ...rt,
147
+ scheduleRunCount: typeof rt.scheduleRunCount === "number" ? rt.scheduleRunCount : 0,
148
+ nextRunAt: rt.nextRunAt ?? undefined,
149
+ lastScheduleAt: rt.lastScheduleAt ?? undefined
150
+ }));
151
+ }
126
152
  return result;
127
153
  }
128
154
  async function writeAtomic(target, contents) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@bojackduy/opencode-loopd",
4
- "version": "1.6.1",
4
+ "version": "1.7.0",
5
5
  "description": "Codex-inspired background goal engine for OpenCode — autonomous subagents, engine-driven loop, child worker sessions and modal TUI dashboard. Like Claude Code loop for OpenCode.",
6
6
  "type": "module",
7
7
  "license": "AGPL-3.0-only",
@@ -0,0 +1,15 @@
1
+ import { mutateState } from "../src/infrastructure/state-repository.ts"
2
+ const dir = "/Users/duytrinh/Code/opencode-loopd"
3
+ const id = "54118aa8-ff7b-471c-b9f4-eab9cf537c41"
4
+ await mutateState(dir, "fix-lease", async (s)=>{
5
+ const r = s.runtimes.find(x=>x.goalID===id)
6
+ if (r) {
7
+ r.leaseExpiresAt = undefined
8
+ r.turnStartedAt = undefined
9
+ r.activeRunID = undefined
10
+ r.activePromptMessageID = undefined
11
+ console.log("cleared lease", r.phase, r.leaseExpiresAt)
12
+ }
13
+ return s
14
+ })
15
+ console.log("fixed")
@@ -0,0 +1,15 @@
1
+ import { readState } from "../src/infrastructure/state-repository.ts"
2
+ import { createScheduleWorker } from "../src/application/schedule-worker.ts"
3
+ import { createGoalService } from "../src/application/goal-service.ts"
4
+ const dir = "/Users/duytrinh/Code/opencode-loopd"
5
+ const mockHost = { createWorker: async()=>{}, promptWorker: async()=>{}, sessionStatus: async()=>"idle", abortSession: async()=>{}, readMessages: async()=>[], compactSession: async()=>{}, notifyOwner: async(e)=>console.log("notify",e)}
6
+ const svc = createGoalService(mockHost)
7
+ svc.continueTurn = async (d, id)=>{ console.log("continueTurn", id)}
8
+ const w = createScheduleWorker({directory: dir, goalService: svc})
9
+ const state = await readState(dir)
10
+ for (const g of state.goals) if (g.id==="54118aa8-ff7b-471c-b9f4-eab9cf537c41") console.log("goal", g.config.schedule, state.runtimes.find(r=>r.goalID===g.id))
11
+ console.log("tick...")
12
+ const n = await w.tick()
13
+ console.log("resurrected", n)
14
+ const after = await readState(dir)
15
+ console.log("after", after.goals.find(g=>g.id==="54118aa8-ff7b-471c-b9f4-eab9cf537c41").status, after.runtimes.find(r=>r.goalID==="54118aa8-ff7b-471c-b9f4-eab9cf537c41"))
@@ -0,0 +1,19 @@
1
+ import { readState, mutateState } from "../src/infrastructure/state-repository.ts"
2
+ const dir = "/Users/duytrinh/Code/opencode-loopd"
3
+ const id = "54118aa8-ff7b-471c-b9f4-eab9cf537c41"
4
+ await mutateState(dir, "patch-schedule", async (s)=>{
5
+ const g = s.goals.find(x=>x.id===id)
6
+ if (g) {
7
+ g.config.schedule = { everyMs: 10000, maxRuns: 3 }
8
+ console.log("patched", g.config.schedule)
9
+ }
10
+ const r = s.runtimes.find(x=>x.goalID===id)
11
+ if (r) {
12
+ r.scheduleRunCount = 0
13
+ console.log("runtime before", r.scheduleRunCount)
14
+ }
15
+ return s
16
+ })
17
+ const state = await readState(dir)
18
+ const g = state.goals.find(x=>x.id===id)
19
+ console.log("after", g.config.schedule)