Class: Agent::Lock::Identity

Inherits:
Object
  • Object
show all
Defined in:
lib/agent/lock/identity.rb

Overview

Who is asking, and whether they are still around.

A lock is worthless unless the holder gives the same answer every time it is asked. The obvious answer, this process's own pid, is the wrong one: an agent harness runs each command in a shell of its own, so a lock acquired by one invocation could never be released by the next. The identity has to belong to the session, not to the process that happens to be speaking for it right now.

In order of preference:

AGENT_ID             what a human or a harness set on purpose
CLAUDE_SESSION_ID    the session, which survives --resume
fingerprint          the first ancestor process that is not a shell

The fingerprint is the fallback that needs explaining. Walking up from this process, the shells are throwaway and the thing above them is not: the claude or codex process driving the session, or the terminal a human is typing in. Its pid and start time, hashed, are stable for as long as that session lives and different for anybody else's.

Constant Summary collapse

SHELLS =
%w[sh bash zsh ksh csh tcsh fish dash].freeze
MAX_HOPS =
10

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(env: ENV) ⇒ Identity

Returns a new instance of Identity.

Parameters:

  • env (Hash) (defaults to: ENV)


50
51
52
# File 'lib/agent/lock/identity.rb', line 50

def initialize(env: ENV)
  @env = env
end

Class Method Details

.current ⇒ Identity

Deliberately not memoized. A forked CLI computes this once and dies, but the same code runs inside a long-lived process during the specs and inside any harness that loads the library, where a cached answer would outlive the environment it was computed from.

Returns:



40
# File 'lib/agent/lock/identity.rb', line 40

def current = new

.session_pid ⇒ Integer

The ancestry walk, on the other hand, cannot change while this process lives, and it costs two ps calls per hop.

Returns:

  • (Integer)


46
# File 'lib/agent/lock/identity.rb', line 46

def session_pid = @session_pid ||= yield

Instance Method Details

#evidence ⇒ Hash{Symbol => Object}

Evidence, not identity: enough to ask later whether the holder is still running. The pid alone is not enough, since pids are reused, and the host matters because a lock store can be on a shared or synced volume.

Returns:

  • (Hash{Symbol => Object})


103
104
105
# File 'lib/agent/lock/identity.rb', line 103

def evidence
  { pid: session_pid, started: ProcessInfo.started_at(session_pid), host: Socket.gethostname }
end

#id ⇒ String

Returns the holder name written into a lock.

Returns:

  • (String) —

    the holder name written into a lock



55
56
57
# File 'lib/agent/lock/identity.rb', line 55

def id
  @id ||= explicit || session || fingerprint
end

#parent_id ⇒ String?

The session that spawned this one: AGENT_PARENT_ID when a harness says so, and otherwise inferred.

The inference exists because Claude Code runs a sub-agent inside its parent's own process and sets nothing to tell them apart, so without it every sub-agent resolved to its parent's fingerprint and none of them could ever block another. A sub-agent's one distinguishing mark is the AGENT_ID it was told to use. When that differs from what this session would answer to without it, the session is, by elimination, the parent.

Returns:

  • (String, nil)


71
72
73
74
75
# File 'lib/agent/lock/identity.rb', line 71

def parent_id
  return @parent_id if defined?(@parent_id)

  @parent_id = declared_parent || inferred_parent
end

#parent_source ⇒ Symbol?

Where #parent_id came from. An inferred parent is a guess a human may want to overrule with AGENT_PARENT_ID, so it is reported as one.

Returns:

  • (Symbol, nil) —

    :explicit, :inferred, or nil when there is no parent



92
93
94
95
96
# File 'lib/agent/lock/identity.rb', line 92

def parent_source
  return :explicit if declared_parent

  :inferred if parent_id
end

#source ⇒ Symbol

Where #id came from, so a session can check what it is being taken for before a lock is written under the wrong name.

Returns:

  • (Symbol) —

    :explicit, :session or :fingerprint



81
82
83
84
85
86
# File 'lib/agent/lock/identity.rb', line 81

def source
  return :explicit if explicit
  return :session if session

  :fingerprint
end