ace-herdr 0.5.1 → 0.6.1
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/CHANGELOG.md +16 -0
- data/lib/ace/herdr/atoms/native_agent_session.rb +32 -0
- data/lib/ace/herdr/models/delivery_record.rb +9 -0
- data/lib/ace/herdr/molecules/bounded_observation.rb +51 -0
- data/lib/ace/herdr/organisms/inbox.rb +39 -16
- data/lib/ace/herdr/organisms/inbox_context_owner.rb +1 -6
- data/lib/ace/herdr/organisms/runtime_adapter.rb +3 -4
- data/lib/ace/herdr/version.rb +1 -1
- metadata +6 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cff3b8e4bd486d4b0e4e9a914fbdfe7010ffd1bc2a2be82babd93acf273e875d
|
|
4
|
+
data.tar.gz: 1b2e18f9c0faef285ff1e4003ff65b49565227a95ce4d80321cf43108486ff8c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ccc1d8b7fc65614ff262c9a84cf431e0a964057426a49e3cd981a38909c85f22752d31bb3b0924f26ccee69eb74487fa41a46a4fef8ab50fcd607cda64f921bd
|
|
7
|
+
data.tar.gz: 0c0f22e240507331336df50b2ebc8966a4b390ae6728b6ccb86b1628eba228d6a82ffa81db19439d2974d124a015bf06a02770558dec1911b40b22a51d2131e6
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.6.1] - 2026-10-16
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Share native immutable session normalization between Inbox and process admission so real Pi path selectors bind their session UUID without weakening ancestry, foreground owner or live-session checks.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Follow the ace-hitl-contract 0.3 request-envelope line (dependency floor).
|
|
19
|
+
|
|
20
|
+
## [0.6.0] - 2026-10-16
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- Expose bounded native observations and hold ordinary Inbox delivery settlement through canonical Assign acceptance. Preserve exact targets and unresolved outcomes without repeat sends or alternate execution records.
|
|
25
|
+
|
|
10
26
|
## [0.5.1] - 2026-10-10
|
|
11
27
|
|
|
12
28
|
### Fixed
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Herdr
|
|
5
|
+
module Atoms
|
|
6
|
+
# Herdr reports Pi sessions as paths and Codex sessions as IDs. Both
|
|
7
|
+
# consumers bind the same immutable UUID; names never identify a session.
|
|
8
|
+
module NativeAgentSession
|
|
9
|
+
UUID = /\A[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}\z/
|
|
10
|
+
PI_PATH = /_([0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12})\.jsonl\z/
|
|
11
|
+
|
|
12
|
+
def self.normalize(session, agent:)
|
|
13
|
+
return nil unless %w[codex pi].include?(agent) && session.is_a?(Hash) && session["agent"] == agent
|
|
14
|
+
|
|
15
|
+
value = session["value"]
|
|
16
|
+
return nil unless value.is_a?(String) && value.valid_encoding? && !value.include?("\0")
|
|
17
|
+
|
|
18
|
+
kind = session["kind"]
|
|
19
|
+
if agent == "pi" && kind == "path"
|
|
20
|
+
return nil unless value.start_with?("/") && value.bytesize <= 4096
|
|
21
|
+
|
|
22
|
+
value = PI_PATH.match(value)&.captures&.first
|
|
23
|
+
kind = "id"
|
|
24
|
+
end
|
|
25
|
+
return nil unless kind == "id" && value.is_a?(String) && UUID.match?(value)
|
|
26
|
+
|
|
27
|
+
{"agent" => agent, "kind" => "id", "value" => value}
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -79,6 +79,15 @@ module Ace
|
|
|
79
79
|
state == "delivered"
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
+
# Exact native submission receipt, not a business completion verdict.
|
|
83
|
+
def inbox_delivered?
|
|
84
|
+
return false unless delivered? && inbox.is_a?(Hash)
|
|
85
|
+
receipt = inbox["receipt"]
|
|
86
|
+
inbox["claim_generation"].is_a?(Integer) && inbox["claim_generation"].positive? &&
|
|
87
|
+
receipt.is_a?(Hash) && receipt.values_at("event_id", "attempt_id", "claim_generation", "payload_sha256", "binding") ==
|
|
88
|
+
[event_id, inbox["attempt_id"], inbox["claim_generation"], answer_digest, inbox["binding"]]
|
|
89
|
+
end
|
|
90
|
+
|
|
82
91
|
# A "submitting" history entry without a matching outcome means the
|
|
83
92
|
# previous run crashed between prompt submission and persistence;
|
|
84
93
|
# resending could duplicate the answer, so the outcome is ambiguous.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "herdr_executor"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Herdr
|
|
7
|
+
module Molecules
|
|
8
|
+
# Read-only native attribution under one absolute deadline, including all
|
|
9
|
+
# ancestry probes. Never renew the budget for another process in the chain.
|
|
10
|
+
class BoundedObservation < HerdrExecutor
|
|
11
|
+
def initialize(deadline:, binary: DEFAULT_BINARY, process: BoundedProcess)
|
|
12
|
+
super(binary: binary, process: process)
|
|
13
|
+
@deadline = deadline
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def pane_get(pane) = observe!([@binary, "pane", "get", pane])
|
|
17
|
+
def pane_process_info(pane) = observe!([@binary, "pane", "process-info", "--pane", pane])
|
|
18
|
+
|
|
19
|
+
# ProcessIdentity's existing executor seam. Do not let PATH select ps.
|
|
20
|
+
def capture2(command, *args, stdin_data: "")
|
|
21
|
+
raise ArgumentError, "observation only permits ps" unless command == "ps" && stdin_data.empty?
|
|
22
|
+
result = bounded(["/bin/ps", *args])
|
|
23
|
+
[result.stdout, result.status]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def observe!(argv)
|
|
29
|
+
result = bounded(argv)
|
|
30
|
+
execution = ExecutionResult.new(stdout: result.stdout.strip, stderr: result.stderr.strip,
|
|
31
|
+
success: result.status.success?, exit_code: result.status.exitstatus || -1)
|
|
32
|
+
raise classify(execution, argv) unless execution.success?
|
|
33
|
+
execution
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def bounded(argv)
|
|
37
|
+
remaining = @deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
38
|
+
raise AgentNotReadyError, "native observation deadline elapsed" unless remaining.positive?
|
|
39
|
+
result = @process.call(argv, stdin_data: "", timeout_s: remaining,
|
|
40
|
+
output_limit: WAKE_OUTPUT_LIMIT, cleanup_group: true)
|
|
41
|
+
raise AgentNotReadyError, "native observation output exceeded limit" if result.oversized
|
|
42
|
+
result
|
|
43
|
+
rescue Timeout::Error, BoundedProcess::PostLaunchError
|
|
44
|
+
raise AgentNotReadyError, "native observation did not complete within its bound"
|
|
45
|
+
rescue SystemCallError
|
|
46
|
+
raise ExecutorUnavailableError, "native observation executable unavailable"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../errors"
|
|
4
|
+
require_relative "../atoms/native_agent_session"
|
|
4
5
|
require "digest"
|
|
5
6
|
require "json"
|
|
6
7
|
require "time"
|
|
@@ -22,8 +23,6 @@ module Ace
|
|
|
22
23
|
|
|
23
24
|
EVENT = /\A[A-Za-z0-9][A-Za-z0-9._-]{0,127}\z/
|
|
24
25
|
TARGET_IDENTITY = %w[session pane terminal_id agent thread thread_kind].freeze
|
|
25
|
-
THREAD_ID = /\A[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}\z/
|
|
26
|
-
PI_PATH = /_([0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12})\.jsonl\z/
|
|
27
26
|
# Single validator for Pi queue event ids, shared by enqueue and
|
|
28
27
|
# delivery observation so the two sites can never disagree again.
|
|
29
28
|
PI_EVENT_ID = /\A(?:inb|wnk)-[a-z0-9-]{8,64}\z/
|
|
@@ -172,6 +171,39 @@ module Ace
|
|
|
172
171
|
|
|
173
172
|
def context_root = @deliveries_dir.dup.freeze
|
|
174
173
|
|
|
174
|
+
# The installed consumer supplies canonical references and native
|
|
175
|
+
# owner, while this owner holds every existing record lock through the
|
|
176
|
+
# consumer's acceptance CAS. No delivery/retry/wake is performed here.
|
|
177
|
+
def with_delivery_settlement(deadline: nil)
|
|
178
|
+
self.class.with_retained_records(deliveries_dir: @deliveries_dir, deadline: deadline) do |records|
|
|
179
|
+
verify = lambda do |references:, attempt_id:, owner:|
|
|
180
|
+
unless references.is_a?(Array) && references.all? { |reference|
|
|
181
|
+
reference.is_a?(Hash) && reference.keys.sort == %w[attempt_id event_id payload_sha256] &&
|
|
182
|
+
reference["attempt_id"] == attempt_id }
|
|
183
|
+
raise ValidationError, "canonical Inbox references differ"
|
|
184
|
+
end
|
|
185
|
+
selected = records.select { |record| record.inbox.fetch("attempt_id") == attempt_id }
|
|
186
|
+
ids = references.map { |reference| reference.fetch("event_id") }
|
|
187
|
+
unless ids.uniq == ids && selected.map(&:event_id).sort == ids.sort
|
|
188
|
+
raise ValidationError, "canonical Inbox reference inventory differs"
|
|
189
|
+
end
|
|
190
|
+
next [] if selected.empty?
|
|
191
|
+
target = target_from_owner(owner)
|
|
192
|
+
selected.map do |record|
|
|
193
|
+
reference = references.find { |item| item.fetch("event_id") == record.event_id }
|
|
194
|
+
binding = record.inbox.fetch("binding", {})
|
|
195
|
+
unless !record.inbox["original_context"] && record.inbox_delivered? && record.answer_digest == reference.fetch("payload_sha256") &&
|
|
196
|
+
record.inbox["origin_target"] == target && binding.slice(*target.keys) == target &&
|
|
197
|
+
binding["payload_sha256"] == record.answer_digest && %w[none sent].include?(record.inbox.dig("wake", "status"))
|
|
198
|
+
raise ValidationError, "original Inbox delivery or wake remains unsettled"
|
|
199
|
+
end
|
|
200
|
+
reference.merge("claim_generation" => record.inbox.fetch("claim_generation"))
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
yield verify
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
175
207
|
|
|
176
208
|
def enqueue(event:, attempt:, ref:, payload:, managed_envelope: nil, expected_target: nil, original_context: nil, original_binding_digest: nil)
|
|
177
209
|
validate_id!(event, "event")
|
|
@@ -544,7 +576,7 @@ module Ace
|
|
|
544
576
|
def validate_expected_target!(target)
|
|
545
577
|
unless target.is_a?(Hash) && TARGET_IDENTITY.all? { |key| target[key].is_a?(String) && !target[key].empty? } &&
|
|
546
578
|
%w[codex pi].include?(target["agent"]) && target["thread_kind"] == "id" &&
|
|
547
|
-
|
|
579
|
+
Atoms::NativeAgentSession::UUID.match?(target["thread"]) && target["terminal_id"].bytesize <= 64
|
|
548
580
|
raise ValidationError, "accepted original native target is incomplete or invalid"
|
|
549
581
|
end
|
|
550
582
|
end
|
|
@@ -594,21 +626,12 @@ module Ace
|
|
|
594
626
|
raise ValidationError, "unsupported native agent" unless %w[codex pi].include?(agent)
|
|
595
627
|
session = pane["agent_session"]
|
|
596
628
|
raise IdentityDriftError, "native session identity is unavailable" unless session.is_a?(Hash) && session["agent"] == agent
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
if agent == "pi" && kind == "path"
|
|
600
|
-
thread = PI_PATH.match(thread)&.captures&.first.to_s
|
|
601
|
-
kind = "id"
|
|
602
|
-
end
|
|
603
|
-
# Only an immutable session UUID may carry a binding: a name
|
|
604
|
-
# survives an agent restart in the same pane/terminal, so a reused
|
|
605
|
-
# session could inherit an old event.
|
|
606
|
-
unless kind == "id" && THREAD_ID.match?(thread)
|
|
629
|
+
normalized = Atoms::NativeAgentSession.normalize(session, agent: agent)
|
|
630
|
+
unless normalized
|
|
607
631
|
raise ValidationError,
|
|
608
|
-
"native queue binding requires an immutable session id "
|
|
609
|
-
"(got #{kind.inspect})"
|
|
632
|
+
"native queue binding requires an immutable session id (got #{session['kind'].inspect})"
|
|
610
633
|
end
|
|
611
|
-
|
|
634
|
+
thread, kind = normalized.values_at("value", "kind")
|
|
612
635
|
raw_terminal = pane["terminal_id"]
|
|
613
636
|
terminal = raw_terminal.to_s.strip if raw_terminal.is_a?(String) || raw_terminal.is_a?(Integer)
|
|
614
637
|
if terminal.nil? || terminal.empty? || terminal.bytesize > 64
|
|
@@ -378,12 +378,7 @@ module Ace
|
|
|
378
378
|
end
|
|
379
379
|
|
|
380
380
|
def completed_record?(record)
|
|
381
|
-
|
|
382
|
-
inbox = record.inbox
|
|
383
|
-
receipt = inbox["receipt"]
|
|
384
|
-
inbox["claim_generation"].is_a?(Integer) && inbox["claim_generation"].positive? &&
|
|
385
|
-
receipt.is_a?(Hash) && receipt.values_at("event_id", "attempt_id", "claim_generation", "payload_sha256", "binding") ==
|
|
386
|
-
[record.event_id, inbox["attempt_id"], inbox["claim_generation"], record.answer_digest, inbox["binding"]]
|
|
381
|
+
record.inbox_delivered?
|
|
387
382
|
end
|
|
388
383
|
|
|
389
384
|
def expected!(state, expected)
|
|
@@ -4,6 +4,7 @@ require "ace/runtime"
|
|
|
4
4
|
require "digest"
|
|
5
5
|
require "fileutils"
|
|
6
6
|
require "json"
|
|
7
|
+
require_relative "../atoms/native_agent_session"
|
|
7
8
|
|
|
8
9
|
module Ace
|
|
9
10
|
module Herdr
|
|
@@ -124,12 +125,10 @@ module Ace
|
|
|
124
125
|
process.is_a?(Hash) && process["pid"] == owner.dig("process_identity", "pid")
|
|
125
126
|
end
|
|
126
127
|
|
|
127
|
-
session = native["agent_session"]
|
|
128
|
+
session = Atoms::NativeAgentSession.normalize(native["agent_session"], agent: native["agent"])
|
|
128
129
|
workspace = native["workspace_id"] || native["session_id"]
|
|
129
130
|
terminal = native["terminal_id"]
|
|
130
|
-
next nil unless session
|
|
131
|
-
session["value"].is_a?(String) && session["value"].match?(Inbox::THREAD_ID) &&
|
|
132
|
-
session["agent"] == native["agent"] && native["agent"].is_a?(String) &&
|
|
131
|
+
next nil unless session &&
|
|
133
132
|
workspace.is_a?(String) && !workspace.empty? &&
|
|
134
133
|
(terminal.is_a?(String) || terminal.is_a?(Integer)) && !terminal.to_s.empty?
|
|
135
134
|
|
data/lib/ace/herdr/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ace-herdr
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michal Czyz
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-10-
|
|
10
|
+
date: 2026-10-16 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: websocket-driver
|
|
@@ -49,14 +49,14 @@ dependencies:
|
|
|
49
49
|
requirements:
|
|
50
50
|
- - "~>"
|
|
51
51
|
- !ruby/object:Gem::Version
|
|
52
|
-
version: 0.
|
|
52
|
+
version: 0.3.0
|
|
53
53
|
type: :runtime
|
|
54
54
|
prerelease: false
|
|
55
55
|
version_requirements: !ruby/object:Gem::Requirement
|
|
56
56
|
requirements:
|
|
57
57
|
- - "~>"
|
|
58
58
|
- !ruby/object:Gem::Version
|
|
59
|
-
version: 0.
|
|
59
|
+
version: 0.3.0
|
|
60
60
|
- !ruby/object:Gem::Dependency
|
|
61
61
|
name: ace-runtime
|
|
62
62
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -219,6 +219,7 @@ files:
|
|
|
219
219
|
- lib/ace/herdr.rb
|
|
220
220
|
- lib/ace/herdr/atoms/answer_digest.rb
|
|
221
221
|
- lib/ace/herdr/atoms/json_find.rb
|
|
222
|
+
- lib/ace/herdr/atoms/native_agent_session.rb
|
|
222
223
|
- lib/ace/herdr/cli.rb
|
|
223
224
|
- lib/ace/herdr/cli/commands/capture.rb
|
|
224
225
|
- lib/ace/herdr/cli/commands/close.rb
|
|
@@ -237,6 +238,7 @@ files:
|
|
|
237
238
|
- lib/ace/herdr/errors.rb
|
|
238
239
|
- lib/ace/herdr/models/delivery_record.rb
|
|
239
240
|
- lib/ace/herdr/models/dispatch_outcome.rb
|
|
241
|
+
- lib/ace/herdr/molecules/bounded_observation.rb
|
|
240
242
|
- lib/ace/herdr/molecules/bounded_process.rb
|
|
241
243
|
- lib/ace/herdr/molecules/delivery_record_store.rb
|
|
242
244
|
- lib/ace/herdr/molecules/guarded_native_origin.rb
|