ace-herdr 0.1.0 → 0.3.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.
- checksums.yaml +4 -4
- data/.ace-defaults/herdr/config.yml +4 -0
- data/CHANGELOG.md +28 -0
- data/README.md +3 -0
- data/docs/usage.md +57 -2
- data/exe/ace-herdr +0 -1
- data/lib/ace/herdr/atoms/json_find.rb +46 -0
- data/lib/ace/herdr/cli/commands/inbox.rb +88 -0
- data/lib/ace/herdr/cli.rb +4 -0
- data/lib/ace/herdr/errors.rb +3 -0
- data/lib/ace/herdr/models/delivery_record.rb +42 -7
- data/lib/ace/herdr/molecules/bounded_process.rb +118 -0
- data/lib/ace/herdr/molecules/delivery_record_store.rb +3 -0
- data/lib/ace/herdr/molecules/herdr_executor.rb +96 -7
- data/lib/ace/herdr/molecules/native_queue_executor.rb +120 -0
- data/lib/ace/herdr/organisms/control_surface.rb +20 -32
- data/lib/ace/herdr/organisms/deliverer.rb +2 -0
- data/lib/ace/herdr/organisms/inbox.rb +484 -0
- data/lib/ace/herdr/organisms/runtime_adapter.rb +555 -0
- data/lib/ace/herdr/organisms/tidy.rb +2 -2
- data/lib/ace/herdr/version.rb +1 -1
- data/lib/ace/herdr.rb +17 -0
- data/lib/ace/runtime/adapters/herdr.rb +6 -0
- metadata +26 -5
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "open3"
|
|
4
4
|
require "json"
|
|
5
|
+
require "timeout"
|
|
5
6
|
|
|
6
7
|
module Ace
|
|
7
8
|
module Herdr
|
|
@@ -12,6 +13,8 @@ module Ace
|
|
|
12
13
|
# binary; tests substitute this class.
|
|
13
14
|
class HerdrExecutor
|
|
14
15
|
DEFAULT_BINARY = "herdr"
|
|
16
|
+
WAKE_OUTPUT_LIMIT = 65_536
|
|
17
|
+
DEFAULT_PROBE_TIMEOUT_S = 60
|
|
15
18
|
|
|
16
19
|
def initialize(binary: DEFAULT_BINARY)
|
|
17
20
|
@binary = binary
|
|
@@ -22,6 +25,28 @@ module Ace
|
|
|
22
25
|
run!([@binary, "agent", "get", pane])
|
|
23
26
|
end
|
|
24
27
|
|
|
28
|
+
# Structured live pane observation for the inbox: the caller holds
|
|
29
|
+
# its per-event lock across this probe, so a stalled herdr child is
|
|
30
|
+
# killed at the deadline and the timeout classifies as a retryable
|
|
31
|
+
# pre-submission failure.
|
|
32
|
+
def pane_get_bounded(pane, timeout_s: DEFAULT_PROBE_TIMEOUT_S)
|
|
33
|
+
result = BoundedProcess.call([@binary, "pane", "get", pane], stdin_data: "",
|
|
34
|
+
timeout_s: timeout_s, output_limit: WAKE_OUTPUT_LIMIT)
|
|
35
|
+
execution = ExecutionResult.new(
|
|
36
|
+
stdout: result.stdout.strip, stderr: result.stderr.strip,
|
|
37
|
+
success: result.status.success?, exit_code: result.status.exitstatus || -1
|
|
38
|
+
)
|
|
39
|
+
raise classify(execution, [@binary, "pane", "get", pane]) unless execution.success?
|
|
40
|
+
|
|
41
|
+
execution
|
|
42
|
+
rescue Timeout::Error
|
|
43
|
+
raise AgentNotReadyError, "pane probe timed out after #{timeout_s}s"
|
|
44
|
+
rescue BoundedProcess::PostLaunchError => e
|
|
45
|
+
raise AgentNotReadyError, "pane probe failed after launch: #{e.message}"
|
|
46
|
+
rescue SystemCallError
|
|
47
|
+
raise ExecutorUnavailableError, "herdr CLI not found or not executable: #{@binary}"
|
|
48
|
+
end
|
|
49
|
+
|
|
25
50
|
# Start an agent in a pane at an interactive shell prompt.
|
|
26
51
|
# Success means the agent was detected and is ready for input.
|
|
27
52
|
def agent_start(name:, kind:, pane:, timeout_ms:)
|
|
@@ -35,6 +60,34 @@ module Ace
|
|
|
35
60
|
run!([@binary, "agent", "prompt", pane, text])
|
|
36
61
|
end
|
|
37
62
|
|
|
63
|
+
# A bounded, payload-free wake. The child deadline is managed by
|
|
64
|
+
# BoundedProcess (process-group kill), so a stalled process cannot
|
|
65
|
+
# keep the inbox event lock through Open3.capture3 cleanup.
|
|
66
|
+
def agent_prompt_bounded(pane:, text:, timeout_ms:)
|
|
67
|
+
cmd = [@binary, "agent", "prompt", pane, text]
|
|
68
|
+
result = BoundedProcess.call(cmd, stdin_data: "",
|
|
69
|
+
timeout_s: timeout_ms / 1000.0, output_limit: WAKE_OUTPUT_LIMIT)
|
|
70
|
+
execution = ExecutionResult.new(
|
|
71
|
+
stdout: result.stdout.strip, stderr: result.stderr.strip,
|
|
72
|
+
success: result.status.success?, exit_code: result.status.exitstatus || -1
|
|
73
|
+
)
|
|
74
|
+
unless execution.success?
|
|
75
|
+
error = classify(execution, cmd)
|
|
76
|
+
if result.oversized && error.is_a?(CommandError)
|
|
77
|
+
raise CommandError, "herdr wake output exceeded #{WAKE_OUTPUT_LIMIT} bytes (exit #{execution.exit_code})"
|
|
78
|
+
end
|
|
79
|
+
raise error
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
execution
|
|
83
|
+
rescue Timeout::Error
|
|
84
|
+
raise AgentNotReadyError, "herdr wake timed out after #{timeout_ms}ms"
|
|
85
|
+
rescue BoundedProcess::PostLaunchError => e
|
|
86
|
+
raise AgentNotReadyError, "herdr wake failed after launch: #{e.message}"
|
|
87
|
+
rescue SystemCallError => e
|
|
88
|
+
raise ExecutorUnavailableError, e.message
|
|
89
|
+
end
|
|
90
|
+
|
|
38
91
|
# Wait until the agent reaches one of the requested states
|
|
39
92
|
def agent_wait(pane:, until_states:, timeout_ms:)
|
|
40
93
|
cmd = [@binary, "agent", "wait", pane]
|
|
@@ -60,6 +113,12 @@ module Ace
|
|
|
60
113
|
run!([@binary, "pane", "current", "--current"])
|
|
61
114
|
end
|
|
62
115
|
|
|
116
|
+
# Structured live pane observation; callers must verify all identity
|
|
117
|
+
# fields before using a native queue target.
|
|
118
|
+
def pane_get(pane)
|
|
119
|
+
run!([@binary, "pane", "get", pane])
|
|
120
|
+
end
|
|
121
|
+
|
|
63
122
|
# Inspect a pane's processes (positive proof of process exit for tidy)
|
|
64
123
|
def pane_process_info(pane)
|
|
65
124
|
run!([@binary, "pane", "process-info", "--pane", pane])
|
|
@@ -73,6 +132,18 @@ module Ace
|
|
|
73
132
|
run!(cmd)
|
|
74
133
|
end
|
|
75
134
|
|
|
135
|
+
def tab_get(tab)
|
|
136
|
+
run!([@binary, "tab", "get", tab])
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def tab_focus(tab)
|
|
140
|
+
run!([@binary, "tab", "focus", tab])
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def api_snapshot
|
|
144
|
+
run!([@binary, "api", "snapshot"])
|
|
145
|
+
end
|
|
146
|
+
|
|
76
147
|
# --- terminal-control surface (spec 8wq.t.k84) -----------------------
|
|
77
148
|
|
|
78
149
|
# List workspaces (tmux sessions analogue)
|
|
@@ -181,8 +252,8 @@ module Ace
|
|
|
181
252
|
stdout: stdout.strip, stderr: stderr.strip,
|
|
182
253
|
success: status.success?, exit_code: status.exitstatus || -1
|
|
183
254
|
)
|
|
184
|
-
rescue
|
|
185
|
-
raise ExecutorUnavailableError, "herdr CLI not found
|
|
255
|
+
rescue SystemCallError
|
|
256
|
+
raise ExecutorUnavailableError, "herdr CLI not found or not executable: #{@binary}"
|
|
186
257
|
end
|
|
187
258
|
|
|
188
259
|
def run_raw_stdout(cmd)
|
|
@@ -191,8 +262,8 @@ module Ace
|
|
|
191
262
|
stdout: stdout, stderr: stderr.strip,
|
|
192
263
|
success: status.success?, exit_code: status.exitstatus || -1
|
|
193
264
|
)
|
|
194
|
-
rescue
|
|
195
|
-
raise ExecutorUnavailableError, "herdr CLI not found
|
|
265
|
+
rescue SystemCallError
|
|
266
|
+
raise ExecutorUnavailableError, "herdr CLI not found or not executable: #{@binary}"
|
|
196
267
|
end
|
|
197
268
|
|
|
198
269
|
# Map a failed result to a typed error from herdr's error codes
|
|
@@ -200,6 +271,25 @@ module Ace
|
|
|
200
271
|
# code is kept in the message so CLI failures carry it.
|
|
201
272
|
def classify(result, cmd)
|
|
202
273
|
code, message = error_code(result)
|
|
274
|
+
if code
|
|
275
|
+
return classify_code(code, message, result: result, cmd: cmd)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# No structured code: first nonempty of stderr/stdout — herdr
|
|
279
|
+
# writes some failures (socket errors) to stdout with an empty
|
|
280
|
+
# stderr, and empty strings are truthy in Ruby, so `||` chaining
|
|
281
|
+
# would hide the detail.
|
|
282
|
+
detail = [result.stderr, result.stdout].reject { |value| value.to_s.empty? }.first
|
|
283
|
+
if detail.to_s.match?(/no herdr server is running|cannot connect|connection refused|socket.*unavailable/i)
|
|
284
|
+
return ExecutorUnavailableError.new(detail)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
CommandError.new(
|
|
288
|
+
"herdr command failed (exit #{result.exit_code}): #{cmd.join(" ")} #{detail}".strip
|
|
289
|
+
)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def classify_code(code, message, result:, cmd:)
|
|
203
293
|
case code
|
|
204
294
|
when "agent_blocked" then AgentBlockedError.new(tag_code(code, message))
|
|
205
295
|
when "agent_prompt_stalled" then AgentNotReadyError.new(tag_code(code, message))
|
|
@@ -207,11 +297,10 @@ module Ace
|
|
|
207
297
|
when "agent_not_found" then AgentNotFoundError.new(tag_code(code, message))
|
|
208
298
|
when "tab_not_found" then TabNotFoundError.new(tag_code(code, message))
|
|
209
299
|
when "workspace_not_found" then WorkspaceNotFoundError.new(tag_code(code, message))
|
|
210
|
-
when "timeout" then
|
|
300
|
+
when "timeout" then ExecutorTimeoutError.new(tag_code(code, message))
|
|
211
301
|
else
|
|
212
302
|
CommandError.new(
|
|
213
|
-
"herdr command failed (exit #{result.exit_code}): #{cmd.join(" ")} "
|
|
214
|
-
"#{message || result.stderr}".strip
|
|
303
|
+
"herdr command failed (exit #{result.exit_code}): #{cmd.join(" ")}: #{tag_code(code, message)}"
|
|
215
304
|
)
|
|
216
305
|
end
|
|
217
306
|
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "timeout"
|
|
5
|
+
|
|
6
|
+
module Ace
|
|
7
|
+
module Herdr
|
|
8
|
+
module Molecules
|
|
9
|
+
# The provider-native submission boundary. A failed process does not
|
|
10
|
+
# prove that a message was not accepted, so only validation failures
|
|
11
|
+
# raised before this boundary are retryable without reconciliation.
|
|
12
|
+
#
|
|
13
|
+
# Child processes run under BoundedProcess: the deadline is enforced at
|
|
14
|
+
# the child boundary (process-group kill), never via a cleanup-blocking
|
|
15
|
+
# Timeout.timeout around capture3, so a stalled child cannot hold the
|
|
16
|
+
# inbox event lock past the configured timeout.
|
|
17
|
+
class NativeQueueExecutor
|
|
18
|
+
DEFAULT_SUBMIT_TIMEOUT_S = 60
|
|
19
|
+
DEFAULT_IDENTITY_TIMEOUT_S = 20
|
|
20
|
+
# Codex payloads ride in argv; a single argument beyond the OS
|
|
21
|
+
# per-argument limit fails at exec with E2BIG — before any client
|
|
22
|
+
# code ran — so the payload is bounded pre-launch and any E2BIG
|
|
23
|
+
# escape is classified as a proven pre-submission rejection.
|
|
24
|
+
MAX_ARG_PAYLOAD_BYTES = 65_536
|
|
25
|
+
PI_PAYLOAD_LIMIT_BYTES = 65_536
|
|
26
|
+
|
|
27
|
+
# runner: test seam. Callable (argv, stdin_data:, timeout_s:) ->
|
|
28
|
+
# [stdout, stderr, status]. Defaults to the bounded runner.
|
|
29
|
+
def initialize(codex: "codex", pi_client: nil, runner: nil,
|
|
30
|
+
submit_timeout_s: DEFAULT_SUBMIT_TIMEOUT_S,
|
|
31
|
+
identity_timeout_s: DEFAULT_IDENTITY_TIMEOUT_S)
|
|
32
|
+
@codex = codex
|
|
33
|
+
@pi_client = pi_client || ENV["ACE_HERDR_PI_QUEUE_CLIENT"] || "pi-overseer-queue-client"
|
|
34
|
+
@runner = runner
|
|
35
|
+
@submit_timeout_s = submit_timeout_s
|
|
36
|
+
@identity_timeout_s = identity_timeout_s
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def pi_identity
|
|
40
|
+
result = execute([@pi_client, "--identity"], stdin_data: "",
|
|
41
|
+
timeout_s: @identity_timeout_s)
|
|
42
|
+
raise ExecutorError, "Pi identity probe failed: #{result.stderr}" unless result.status.success?
|
|
43
|
+
|
|
44
|
+
identity = JSON.parse(result.stdout)
|
|
45
|
+
raise ExecutorError, "Pi identity probe returned invalid JSON" unless identity.is_a?(Hash)
|
|
46
|
+
|
|
47
|
+
identity.fetch("session_id")
|
|
48
|
+
rescue Errno::ENOENT, Errno::EACCES => e
|
|
49
|
+
raise ExecutorUnavailableError, e.message
|
|
50
|
+
rescue JSON::ParserError, KeyError, TypeError
|
|
51
|
+
raise ExecutorError, "Pi identity probe returned invalid JSON"
|
|
52
|
+
rescue Timeout::Error
|
|
53
|
+
raise ExecutorError, "Pi identity probe timed out"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def submit(agent:, thread:, event_id:, digest:, payload:)
|
|
57
|
+
if agent == "pi" && (payload.empty? || payload.bytesize > PI_PAYLOAD_LIMIT_BYTES)
|
|
58
|
+
return {"accepted" => false, "pre_submit" => true, "error" => "Pi payload must be 1..65536 bytes"}
|
|
59
|
+
end
|
|
60
|
+
if agent == "codex" && payload.bytesize > MAX_ARG_PAYLOAD_BYTES
|
|
61
|
+
return {"accepted" => false, "pre_submit" => true,
|
|
62
|
+
"error" => "Codex payload must be at most #{MAX_ARG_PAYLOAD_BYTES} bytes for argv delivery"}
|
|
63
|
+
end
|
|
64
|
+
argv = if agent == "codex"
|
|
65
|
+
[@codex, "queue", "--thread", thread, "--message", payload]
|
|
66
|
+
elsif agent == "pi"
|
|
67
|
+
[@pi_client, "--delivery", event_id, "--session-id", thread,
|
|
68
|
+
"--payload-sha256", digest, "--payload-bytes", payload.bytesize.to_s]
|
|
69
|
+
else
|
|
70
|
+
raise ValidationError, "unsupported native queue agent: #{agent}"
|
|
71
|
+
end
|
|
72
|
+
result = execute(argv, stdin_data: agent == "pi" ? payload : "",
|
|
73
|
+
timeout_s: @submit_timeout_s)
|
|
74
|
+
raise ExecutorError, "native queue output exceeded the retained limit" if result.oversized
|
|
75
|
+
unless result.status.success?
|
|
76
|
+
return {"accepted" => false, "exit_code" => result.status.exitstatus,
|
|
77
|
+
"error" => result.stderr.strip}
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
if agent == "pi"
|
|
81
|
+
receipt = JSON.parse(result.stdout)
|
|
82
|
+
unless receipt.is_a?(Hash) && receipt["ok"] == true && receipt["id"] == event_id &&
|
|
83
|
+
receipt["session_id"] == thread && receipt["payload_sha256"] == digest
|
|
84
|
+
return {"accepted" => false, "error" => "Pi queue receipt identity or digest mismatch"}
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
{"accepted" => true, "exit_code" => result.status.exitstatus, "stdout" => result.stdout.strip}
|
|
88
|
+
rescue Errno::ENOENT, Errno::EACCES, Errno::E2BIG, ExecutorUnavailableError => e
|
|
89
|
+
# The executable never launched (or exec rejected the argv). No
|
|
90
|
+
# submission could have occurred, so this stays retryable.
|
|
91
|
+
{"accepted" => false, "pre_submit" => true, "error" => e.message}
|
|
92
|
+
rescue JSON::ParserError
|
|
93
|
+
{"accepted" => false, "error" => "Pi queue returned invalid receipt JSON"}
|
|
94
|
+
rescue Timeout::Error
|
|
95
|
+
{"accepted" => false, "error" => "native queue timed out after submission may have begun"}
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def execute(argv, stdin_data:, timeout_s:)
|
|
101
|
+
if @runner
|
|
102
|
+
stdout, stderr, status = @runner.call(argv, stdin_data: stdin_data, timeout_s: timeout_s)
|
|
103
|
+
return BoundedProcess::Result.new(stdout, stderr, status, false)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
BoundedProcess.call(argv, stdin_data: stdin_data, timeout_s: timeout_s)
|
|
107
|
+
rescue SystemCallError => e
|
|
108
|
+
# popen3 raises spawn failures before the child exists, so no
|
|
109
|
+
# submission can have begun: proven pre-launch, retryable.
|
|
110
|
+
# (Post-launch I/O failures arrive as PostLaunchError instead.)
|
|
111
|
+
raise ExecutorUnavailableError, e.message
|
|
112
|
+
rescue BoundedProcess::PostLaunchError => e
|
|
113
|
+
# The child was live, so the message may already have been accepted:
|
|
114
|
+
# keep this uncertain, never pre-submit retryable.
|
|
115
|
+
raise ExecutorError, e.message
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -146,9 +146,10 @@ module Ace
|
|
|
146
146
|
# Create a tab from a preset (tmux `window` analogue) in the given
|
|
147
147
|
# or resolved workspace; --cwd overrides the resolved tab cwd. The
|
|
148
148
|
# layout is validated before the tab is created.
|
|
149
|
-
def create_tab(preset_name, workspace_id: nil, cwd: nil)
|
|
149
|
+
def create_tab(preset_name, workspace_id: nil, cwd: nil, label: nil)
|
|
150
150
|
workspace_id = resolve_workspace_id(workspace_id)
|
|
151
151
|
resolved = resolve_tab_preset(preset_name)
|
|
152
|
+
resolved = resolved.merge("label" => label) if label
|
|
152
153
|
preflight_tab!(resolved)
|
|
153
154
|
instantiate_tab(
|
|
154
155
|
resolved, workspace_id: workspace_id,
|
|
@@ -248,13 +249,20 @@ module Ace
|
|
|
248
249
|
tab_id = dig_value(tab_json, %w[result tab tab_id]) ||
|
|
249
250
|
raise(TargetResolutionError, "could not read the new tab id from herdr tab create output")
|
|
250
251
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
252
|
+
begin
|
|
253
|
+
pane_specs = Array(tab_spec["panes"])
|
|
254
|
+
root_pane_id = dig_value(tab_json, %w[result root_pane pane_id]) ||
|
|
255
|
+
raise(TargetResolutionError, "could not read the root pane id from herdr tab create output")
|
|
256
|
+
placed = place_panes(tab_spec, pane_specs, tab_cwd, root_pane_id)
|
|
257
|
+
|
|
258
|
+
commands = run_pane_commands(placed)
|
|
259
|
+
agents = start_pane_agents(placed, workspace_id: workspace_id)
|
|
260
|
+
rescue StandardError => e
|
|
261
|
+
# Surface the created tab id and the original error class so
|
|
262
|
+
# the owner can roll back exactly this tab and keep
|
|
263
|
+
# mapping-relevant semantics (e.g. AgentBlockedError).
|
|
264
|
+
raise TabMaterializationError.new(tab_id: tab_id, message: "#{e.class}: #{e.message}")
|
|
265
|
+
end
|
|
258
266
|
|
|
259
267
|
{
|
|
260
268
|
tab: tab_id,
|
|
@@ -386,33 +394,13 @@ module Ace
|
|
|
386
394
|
end
|
|
387
395
|
|
|
388
396
|
def dig_value(json, path)
|
|
389
|
-
|
|
390
|
-
path.each do |key|
|
|
391
|
-
return nil unless current.is_a?(Hash)
|
|
392
|
-
|
|
393
|
-
current = current[key]
|
|
394
|
-
end
|
|
395
|
-
current.is_a?(String) && !current.empty? ? current : nil
|
|
397
|
+
Atoms::JsonFind.dig_value(json, path)
|
|
396
398
|
end
|
|
397
399
|
|
|
398
|
-
#
|
|
399
|
-
#
|
|
400
|
+
# Tolerant extraction of ids from native responses (native shapes
|
|
401
|
+
# nest under result.*); shared atom, same semantics as Dispatcher.
|
|
400
402
|
def find_value(json, keys)
|
|
401
|
-
|
|
402
|
-
when Hash
|
|
403
|
-
keys.each { |key| return json[key] if json[key].is_a?(String) && !json[key].empty? }
|
|
404
|
-
json.each_value do |value|
|
|
405
|
-
found = find_value(value, keys)
|
|
406
|
-
return found if found
|
|
407
|
-
end
|
|
408
|
-
nil
|
|
409
|
-
when Array
|
|
410
|
-
json.each do |value|
|
|
411
|
-
found = find_value(value, keys)
|
|
412
|
-
return found if found
|
|
413
|
-
end
|
|
414
|
-
nil
|
|
415
|
-
end
|
|
403
|
+
Atoms::JsonFind.find_value(json, keys)
|
|
416
404
|
end
|
|
417
405
|
|
|
418
406
|
# --- send: validation -------------------------------------------------
|
|
@@ -81,6 +81,7 @@ module Ace
|
|
|
81
81
|
|
|
82
82
|
Molecules::DeliveryRecordStore.with_lock(@deliveries_dir, event_id) do
|
|
83
83
|
record = Molecules::DeliveryRecordStore.load(@deliveries_dir, event_id)
|
|
84
|
+
raise ValidationError, "event belongs to the agent inbox" if record&.inbox
|
|
84
85
|
validate_existing_record(record, ref, digest) if record
|
|
85
86
|
record ||= Models::DeliveryRecord.new(
|
|
86
87
|
event_id: event_id, session: ref.session, pane: ref.pane,
|
|
@@ -98,6 +99,7 @@ module Ace
|
|
|
98
99
|
validate_event_id!(event_id)
|
|
99
100
|
Molecules::DeliveryRecordStore.with_lock(@deliveries_dir, event_id) do
|
|
100
101
|
record = Molecules::DeliveryRecordStore.load(@deliveries_dir, event_id)
|
|
102
|
+
raise ValidationError, "event belongs to the agent inbox" if record&.inbox
|
|
101
103
|
if record.nil? || record.answer.to_s.empty?
|
|
102
104
|
raise ValidationError,
|
|
103
105
|
"no recoverable answer stored for event #{event_id.inspect}"
|