Class: Insika::Commands::ResumeTask

Inherits:
Object
  • Object
show all
Defined in:
lib/insika/commands/resume_task.rb

Overview

Turn command: resumes a task from the latest checkpoint. Crash-recovery and manual resume both use THIS same path (Recovery only discovers and dispatches). Re-executes the entire checkpointed turn; non-idempotent tools that already completed are skipped via the side-effect registry.

Instance Method Summary collapse

Constructor Details

#initialize(profiles:, task_store:, checkpoint_store:, executor:) ⇒ ResumeTask

Returns a new instance of ResumeTask.



10
11
12
13
14
15
# File 'lib/insika/commands/resume_task.rb', line 10

def initialize(profiles:, task_store:, checkpoint_store:, executor:)
  @profiles = ProfileSource.coerce(profiles)
  @task_store = task_store
  @checkpoint_store = checkpoint_store
  @executor = executor
end

Instance Method Details

#call(command) ⇒ Object



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/insika/commands/resume_task.rb', line 17

def call(command)
  task_id = (command.payload[:task_id] || command.payload["task_id"]).to_s
  raise Insika::ValidationError, "task_id is required" if task_id.empty?

  task = @task_store.find(task_id) ||
         (raise Insika::NotFoundError, "task '#{task_id}' not found")

  # IN-PROCESS RESUME: a :paused task whose fiber is STILL alive (blocked
  # on await) — do NOT re-dispatch (spawn would duplicate the fiber). Just post :resume;
  # the fiber transitions paused->running and continues. (A live :waiting is resolved
  # by ApproveAction, not here.)
  if task.status == :paused && @executor.running?(task_id)
    @executor.resume_live(task_id)
    return { task_id: task_id }
  end

  # :queued: a turn that was in the SessionActor queue and never
  # started at crash time (no checkpoint) — recovering = RUN from scratch, from the
  # original Command. Profile comes from the agent in the Command itself.
  if task.status == :queued
    @executor.spawn_in_session(task, profile: profile_for(task), resume_from: nil)
    return { task_id: task_id }
  end

  # RE-DISPATCH (crash-resume): no live fiber, re-executes from the checkpoint.
  # resume requires a checkpoint; without one the task is unrecoverable (Recovery
  # would already have marked it :failed during the sweep).
  checkpoint = @checkpoint_store.latest(task_id) ||
               (raise Insika::ValidationError,
                      "task '#{task_id}' has no checkpoint — unrecoverable")

  check_eligibility!(task)

  # profile comes from the checkpoint (agent_id): the agent may have been removed from
  # the config between the crash and boot -> fail loudly and clearly.
  profile = @profiles[checkpoint.agent_id] ||
            (raise Insika::NotFoundError, "agent '#{checkpoint.agent_id}' not configured")

  @executor.spawn_in_session(task, profile: profile, resume_from: checkpoint)
  { task_id: task_id }
end