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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 00422d9f6820e48b0ea45698e4663f7ecaeb9b5b01ddf1626ab86aed50adcbfd
4
- data.tar.gz: 2632e4056271d3c022056a70fa5f3b95e2f69ae08544bf70f2577bc11fbac4f7
3
+ metadata.gz: cff3b8e4bd486d4b0e4e9a914fbdfe7010ffd1bc2a2be82babd93acf273e875d
4
+ data.tar.gz: 1b2e18f9c0faef285ff1e4003ff65b49565227a95ce4d80321cf43108486ff8c
5
5
  SHA512:
6
- metadata.gz: c6626094540195dadf06116add8bd27708934b809347c12cb0bc355cb6c03d5ff32e30e3e63870e158316cf9c80de7e2bdd5cad5f731cea4fef11303c1c9bd8a
7
- data.tar.gz: 7b4e02b683be089a602d10c7fafcb09abc134a9f5b60e5a58a8d0ef42b35bb0ec18555700e0c316ec5de12aae7e5c01a4f0ffede12e26597f1a881fcbb5d50af
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
- THREAD_ID.match?(target["thread"]) && target["terminal_id"].bytesize <= 64
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
- thread = session["value"].to_s
598
- kind = session["kind"]
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
- raise ValidationError, "Pi queue requires a session ID" if agent == "pi" && kind != "id"
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
- return false unless record.state == "delivered" && record.inbox.is_a?(Hash)
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.is_a?(Hash) && session["kind"] == "id" &&
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
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ace
4
4
  module Herdr
5
- VERSION = "0.5.1"
5
+ VERSION = "0.6.1"
6
6
  end
7
7
  end
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.5.1
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 00:00:00.000000000 Z
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.2.1
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.2.1
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