pwn 0.5.781 → 0.5.783
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/documentation/Installation.md +33 -9
- data/documentation/diagrams/dot/extrospection-world-awareness.dot +37 -92
- data/documentation/diagrams/dot/memory-skills-detailed.dot +33 -98
- data/documentation/diagrams/dot/overall-pwn-architecture.dot +54 -32
- data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +53 -30
- data/documentation/diagrams/dot/reinforcement-learning.dot +46 -111
- data/documentation/diagrams/dot/swarm-multi-agent.dot +36 -61
- data/documentation/diagrams/extrospection-world-awareness.svg +139 -510
- data/documentation/diagrams/memory-skills-detailed.svg +126 -475
- data/documentation/diagrams/overall-pwn-architecture.svg +116 -266
- data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +158 -244
- data/documentation/diagrams/reinforcement-learning.svg +238 -529
- data/documentation/diagrams/swarm-multi-agent.svg +104 -233
- data/documentation/pwn-REPL.md +31 -3
- data/documentation/pwn-ai-Agent.md +62 -2
- data/etc/default_cron/session_cleanup.rb +335 -42
- data/lib/pwn/ai/agent/curriculum.rb +2 -2
- data/lib/pwn/ai/agent/learning.rb +11 -4
- data/lib/pwn/ai/agent/mistakes.rb +22 -3
- data/lib/pwn/ai/agent/tools/sessions.rb +6 -6
- data/lib/pwn/ai/http_retry.rb +3 -0
- data/lib/pwn/ai/router.rb +3 -1
- data/lib/pwn/cron.rb +1 -1
- data/lib/pwn/memory.rb +62 -6
- data/lib/pwn/plugins/repl/ai.rb +13 -3
- data/lib/pwn/plugins/repl/ai_console.rb +37 -21
- data/lib/pwn/session_trace.rb +1 -1
- data/lib/pwn/sessions.rb +305 -80
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/learning_spec.rb +65 -2
- data/spec/lib/pwn/ai/agent/swarm_spec.rb +3 -3
- data/spec/lib/pwn/memory_spec.rb +58 -1
- data/spec/lib/pwn/plugins/repl/ai_console_spec.rb +191 -6
- data/spec/lib/pwn/plugins/repl/ai_result_chronology_spec.rb +2 -2
- data/spec/lib/pwn/plugins/repl/ai_spec.rb +28 -11
- data/spec/lib/pwn/session_cleanup_default_spec.rb +2 -2
- data/spec/lib/pwn/session_cleanup_payload_spec.rb +467 -36
- data/spec/lib/pwn/sessions_spec.rb +116 -16
- data/third_party/pwn_rdoc.jsonl +12 -0
- metadata +1 -1
data/lib/pwn/ai/router.rb
CHANGED
|
@@ -226,7 +226,9 @@ module PWN
|
|
|
226
226
|
private_class_method def self.env_dig(opts = {})
|
|
227
227
|
return {} unless defined?(PWN::Env) && PWN::Env.respond_to?(:dig)
|
|
228
228
|
|
|
229
|
-
|
|
229
|
+
keys = opts[:keys]
|
|
230
|
+
config = Thread.current[:pwn_session_title_config]
|
|
231
|
+
val = config && keys.first == :ai ? config.dig(*keys.drop(1)) : PWN::Env.dig(*keys)
|
|
230
232
|
if val.is_a?(Hash)
|
|
231
233
|
val.transform_keys { |key| key.is_a?(String) ? key.to_sym : key }
|
|
232
234
|
else
|
data/lib/pwn/cron.rb
CHANGED
|
@@ -255,7 +255,7 @@ module PWN
|
|
|
255
255
|
{
|
|
256
256
|
name: 'session_cleanup',
|
|
257
257
|
schedule: '45 3 * * *',
|
|
258
|
-
ruby: "require File.join(PWN::Cron.cron_dir, 'session_cleanup.rb'); PWN::SessionCleanup.
|
|
258
|
+
ruby: "require File.join(PWN::Cron.cron_dir, 'session_cleanup.rb'); PWN::SessionCleanup.maintain(dry_run: false)",
|
|
259
259
|
enabled: true
|
|
260
260
|
},
|
|
261
261
|
{
|
data/lib/pwn/memory.rb
CHANGED
|
@@ -23,6 +23,31 @@ module PWN
|
|
|
23
23
|
EPHEMERAL_KEY_PREFIXES = %w[session_].freeze
|
|
24
24
|
EPHEMERAL_TTL_SECS = 7 * 86_400
|
|
25
25
|
|
|
26
|
+
# Permanent writer inode; nested calls share ownership only in this thread
|
|
27
|
+
# and process. Never unlink this lock when replacing a data file.
|
|
28
|
+
public_class_method def self.transaction(opts = {})
|
|
29
|
+
path = File.expand_path(opts[:path] || MEMORY_FILE)
|
|
30
|
+
return yield if transaction_owned?(path: path)
|
|
31
|
+
|
|
32
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
33
|
+
File.open("#{path}.write.lock", File::RDWR | File::CREAT | File::NOFOLLOW, 0o600) do |lock|
|
|
34
|
+
lock.flock(File::LOCK_EX)
|
|
35
|
+
owners = (Thread.current[:pwn_store_transactions] ||= {})
|
|
36
|
+
owners[path] = Process.pid
|
|
37
|
+
begin
|
|
38
|
+
yield
|
|
39
|
+
ensure
|
|
40
|
+
owners.delete(path)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Test ownership without mistaking an inherited post-fork marker for a lock.
|
|
46
|
+
public_class_method def self.transaction_owned?(opts = {})
|
|
47
|
+
path = File.expand_path(opts[:path] || MEMORY_FILE)
|
|
48
|
+
Thread.current[:pwn_store_transactions]&.fetch(path, nil) == Process.pid
|
|
49
|
+
end
|
|
50
|
+
|
|
26
51
|
# Supported Method Parameters::
|
|
27
52
|
# memory = PWN::Memory.load
|
|
28
53
|
public_class_method def self.load
|
|
@@ -47,6 +72,8 @@ module PWN
|
|
|
47
72
|
# Supported Method Parameters::
|
|
48
73
|
# PWN::Memory.save(mem: memory_hash)
|
|
49
74
|
public_class_method def self.save(opts = {})
|
|
75
|
+
return transaction(path: MEMORY_FILE) { save(opts) } unless PWN::Memory.transaction_owned?(path: MEMORY_FILE)
|
|
76
|
+
|
|
50
77
|
mem = opts[:mem] ||= {}
|
|
51
78
|
force = opts[:force] ? true : false
|
|
52
79
|
FileUtils.mkdir_p(File.dirname(MEMORY_FILE))
|
|
@@ -58,8 +85,15 @@ module PWN
|
|
|
58
85
|
warn "[pwn-ai/memory] refusing empty overwrite of #{File.size(path)}B #{path} (pass force:true to clear)"
|
|
59
86
|
return load_raw_or_empty(path: path)
|
|
60
87
|
end
|
|
88
|
+
atomic_write(path: path, body: JSON.pretty_generate(mem))
|
|
89
|
+
mem
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Caller holds the permanent store transaction across read and replacement.
|
|
93
|
+
private_class_method def self.atomic_write(opts = {})
|
|
94
|
+
path = opts[:path]
|
|
95
|
+
body = opts[:body]
|
|
61
96
|
tmp = File.join(File.dirname(path), ".#{File.basename(path)}.#{Process.pid}.tmp")
|
|
62
|
-
body = JSON.pretty_generate(mem)
|
|
63
97
|
File.open(tmp, File::WRONLY | File::CREAT | File::TRUNC, 0o644) do |f|
|
|
64
98
|
f.flock(File::LOCK_EX)
|
|
65
99
|
f.write(body)
|
|
@@ -67,7 +101,7 @@ module PWN
|
|
|
67
101
|
f.fsync
|
|
68
102
|
end
|
|
69
103
|
File.rename(tmp, path)
|
|
70
|
-
|
|
104
|
+
File.open(File.dirname(path), File::RDONLY, &:fsync)
|
|
71
105
|
ensure
|
|
72
106
|
FileUtils.rm_f(tmp) if defined?(tmp) && tmp && File.exist?(tmp)
|
|
73
107
|
end
|
|
@@ -93,6 +127,8 @@ module PWN
|
|
|
93
127
|
# ttl: 'optional - seconds until stale (M3; consolidate evicts stale low-conf first)'
|
|
94
128
|
# )
|
|
95
129
|
public_class_method def self.remember(opts = {})
|
|
130
|
+
return transaction(path: MEMORY_FILE) { remember(opts) } unless PWN::Memory.transaction_owned?(path: MEMORY_FILE)
|
|
131
|
+
|
|
96
132
|
key = opts[:key]
|
|
97
133
|
value = opts[:value]
|
|
98
134
|
category = opts[:category] || :fact
|
|
@@ -479,7 +515,9 @@ module PWN
|
|
|
479
515
|
# force: 'optional - Boolean bypass protect policy (default false)'
|
|
480
516
|
# )
|
|
481
517
|
# Refuses PROTECT_KEY_PREFIXES / PROTECT_CATEGORIES unless force:true.
|
|
482
|
-
public_class_method def self.forget(opts = {})
|
|
518
|
+
public_class_method def self.forget(opts = {})
|
|
519
|
+
return transaction(path: MEMORY_FILE) { forget(opts) } unless PWN::Memory.transaction_owned?(path: MEMORY_FILE)
|
|
520
|
+
|
|
483
521
|
key = opts[:key]
|
|
484
522
|
raise 'ERROR: key is required' if key.nil?
|
|
485
523
|
|
|
@@ -498,6 +536,8 @@ module PWN
|
|
|
498
536
|
# PWN::Memory.clear(force: true)
|
|
499
537
|
# Requires force:true — protected prefs/SOPs must not vanish via bare clear.
|
|
500
538
|
public_class_method def self.clear(opts = {})
|
|
539
|
+
return transaction(path: MEMORY_FILE) { clear(opts) } unless PWN::Memory.transaction_owned?(path: MEMORY_FILE)
|
|
540
|
+
|
|
501
541
|
force = if opts.is_a?(Hash)
|
|
502
542
|
opts[:force] ? true : false
|
|
503
543
|
else
|
|
@@ -514,6 +554,8 @@ module PWN
|
|
|
514
554
|
# context = PWN::Memory.to_context(limit: 20)
|
|
515
555
|
# (used internally by pwn-ai hook to inject into system prompt)
|
|
516
556
|
public_class_method def self.to_context(opts = {})
|
|
557
|
+
return transaction(path: MEMORY_FILE) { to_context(opts) } unless PWN::Memory.transaction_owned?(path: MEMORY_FILE)
|
|
558
|
+
|
|
517
559
|
limit = (opts[:limit] || 12).to_i
|
|
518
560
|
limit = 12 if limit <= 0
|
|
519
561
|
mem = load
|
|
@@ -556,14 +598,20 @@ module PWN
|
|
|
556
598
|
end
|
|
557
599
|
|
|
558
600
|
private_class_method def self.archive_expired!(opts = {})
|
|
601
|
+
return transaction(path: MEMORY_FILE) { archive_expired!(opts) } unless PWN::Memory.transaction_owned?(path: MEMORY_FILE)
|
|
602
|
+
|
|
559
603
|
rows = opts[:rows]
|
|
560
604
|
return if rows.nil? || rows.empty?
|
|
561
605
|
|
|
606
|
+
mem = load
|
|
607
|
+
rows = rows.select { |key, value| mem[key] == value }
|
|
608
|
+
return if rows.empty?
|
|
609
|
+
|
|
562
610
|
arch = File.file?(ARCHIVE_FILE) ? JSON.parse(File.read(ARCHIVE_FILE), symbolize_names: true) : {}
|
|
611
|
+
raise IOError, 'Invalid memory archive' unless arch.is_a?(Hash)
|
|
612
|
+
|
|
563
613
|
arch.merge!(rows)
|
|
564
|
-
|
|
565
|
-
File.write(ARCHIVE_FILE, JSON.pretty_generate(arch))
|
|
566
|
-
mem = load
|
|
614
|
+
atomic_write(path: ARCHIVE_FILE, body: JSON.pretty_generate(arch))
|
|
567
615
|
rows.each_key { |k| mem.delete(k) }
|
|
568
616
|
save(mem: mem, force: mem.empty?)
|
|
569
617
|
rescue StandardError
|
|
@@ -590,6 +638,8 @@ module PWN
|
|
|
590
638
|
# Compact overlong values and drop expired ephemeral session_* keys.
|
|
591
639
|
# Never removes protected prefs/SOPs/fix lessons. Safe with empty-save guard.
|
|
592
640
|
public_class_method def self.lean!(opts = {})
|
|
641
|
+
return transaction(path: MEMORY_FILE) { lean!(opts) } unless PWN::Memory.transaction_owned?(path: MEMORY_FILE)
|
|
642
|
+
|
|
593
643
|
dry = opts[:dry_run] ? true : false
|
|
594
644
|
vmax = (opts[:value_max_chars] || VALUE_MAX_CHARS).to_i
|
|
595
645
|
ttl_secs = (opts[:ephemeral_ttl_secs] || EPHEMERAL_TTL_SECS).to_i
|
|
@@ -650,6 +700,12 @@ module PWN
|
|
|
650
700
|
# Display Usage for this Module
|
|
651
701
|
public_class_method def self.help
|
|
652
702
|
puts "USAGE:
|
|
703
|
+
# Serialize a complete read-modify-write operation.
|
|
704
|
+
#{self}.transaction(path: 'optional - data store path, defaults to memory') { operation }
|
|
705
|
+
|
|
706
|
+
# Check current process and thread ownership.
|
|
707
|
+
#{self}.transaction_owned?(path: 'optional - data store path, defaults to memory')
|
|
708
|
+
|
|
653
709
|
# Run load and return its result
|
|
654
710
|
#{self}.load
|
|
655
711
|
|
data/lib/pwn/plugins/repl/ai.rb
CHANGED
|
@@ -129,7 +129,9 @@ module PWN
|
|
|
129
129
|
case cmd
|
|
130
130
|
when 'list', nil
|
|
131
131
|
output.puts('Session dependencies: referenced by these files; text mentions are not links.')
|
|
132
|
-
PWN::Plugins::REPL.pwn_ai_print_result(value: PWN::Sessions.
|
|
132
|
+
PWN::Plugins::REPL.pwn_ai_print_result(value: PWN::Sessions.catalog(offset: args[1], limit: args[2] || 25, current_session_id: pry_instance.config.pwn_ai_session_id), output: output)
|
|
133
|
+
when 'cleanup'
|
|
134
|
+
PWN::Plugins::REPL.pwn_ai_run_sessions(args: args)
|
|
133
135
|
when 'resume'
|
|
134
136
|
sid = args[1]
|
|
135
137
|
hist = PWN::Sessions.to_response_history(session_id: sid)
|
|
@@ -262,7 +264,7 @@ module PWN
|
|
|
262
264
|
'/mcp' => %w[list backends use current connect disconnect ping tools call status help],
|
|
263
265
|
'/model' => %w[list],
|
|
264
266
|
'/swarm' => %w[mission agents status create use spawn retire dm debate broadcast tail steer pause resume cancel help],
|
|
265
|
-
'/sessions' => %w[list resume delete stats],
|
|
267
|
+
'/sessions' => %w[list resume delete stats cleanup],
|
|
266
268
|
|
|
267
269
|
'/skills' => %w[list recall],
|
|
268
270
|
'/learning' => %w[list requeue]
|
|
@@ -599,6 +601,8 @@ module PWN
|
|
|
599
601
|
end
|
|
600
602
|
true
|
|
601
603
|
rescue StandardError => e
|
|
604
|
+
raise if opts[:raise_errors]
|
|
605
|
+
|
|
602
606
|
warn "[pwn-ai] #{cmd}: #{e.class}: #{e.message}"
|
|
603
607
|
true
|
|
604
608
|
end
|
|
@@ -1038,7 +1042,13 @@ module PWN
|
|
|
1038
1042
|
case sub
|
|
1039
1043
|
when 'list'
|
|
1040
1044
|
puts('Session dependencies: referenced by these files; text mentions are not links.')
|
|
1041
|
-
pwn_ai_print_result(value: PWN::Sessions.
|
|
1045
|
+
pwn_ai_print_result(value: PWN::Sessions.catalog(offset: args[1], limit: args[2] || 25, current_session_id: opts[:pry]&.config&.pwn_ai_session_id))
|
|
1046
|
+
when 'cleanup'
|
|
1047
|
+
raise ArgumentError, 'usage: /sessions cleanup [--dry-run]' unless args.drop(1).empty? || args.drop(1) == ['--dry-run']
|
|
1048
|
+
|
|
1049
|
+
require File.expand_path('../../../../etc/default_cron/session_cleanup', __dir__)
|
|
1050
|
+
result = PWN::SessionCleanup.maintain(dry_run: args.include?('--dry-run'), current_session_id: opts[:pry]&.config&.pwn_ai_session_id)
|
|
1051
|
+
pwn_ai_print_result(value: PWN::SessionCleanup.summary(result))
|
|
1042
1052
|
when 'resume'
|
|
1043
1053
|
sid = PWN::Sessions.resolve(identifier: opts[:identifier] || args.drop(1).join(' '))
|
|
1044
1054
|
lease = PWN::Sessions.activate(session_id: sid)
|
|
@@ -637,15 +637,14 @@ module PWN
|
|
|
637
637
|
finish_request_metrics
|
|
638
638
|
@running = false
|
|
639
639
|
@control = nil
|
|
640
|
-
queue_title_update(text) if text
|
|
640
|
+
queue_title_update(*text) if text
|
|
641
641
|
when :session_title
|
|
642
642
|
@session_metadata = text if text && text[:id] == @pry.config.pwn_ai_session_id
|
|
643
643
|
when :title_done
|
|
644
644
|
@title_worker.join
|
|
645
645
|
@title_worker = nil
|
|
646
|
-
pending = @title_pending
|
|
647
|
-
|
|
648
|
-
queue_title_update(pending) if pending
|
|
646
|
+
pending = @title_pending&.shift
|
|
647
|
+
queue_title_update(*pending) if pending
|
|
649
648
|
else
|
|
650
649
|
add(type, text)
|
|
651
650
|
end
|
|
@@ -654,13 +653,16 @@ module PWN
|
|
|
654
653
|
nil
|
|
655
654
|
end
|
|
656
655
|
|
|
657
|
-
def queue_title_update(session_id)
|
|
656
|
+
def queue_title_update(session_id, ai_config = Marshal.load(Marshal.dump(PWN::Env[:ai])))
|
|
657
|
+
@title_pending ||= {}
|
|
658
658
|
if @title_worker
|
|
659
|
-
@title_pending =
|
|
659
|
+
@title_pending[session_id] = ai_config
|
|
660
660
|
return
|
|
661
661
|
end
|
|
662
662
|
@title_worker = Thread.new do
|
|
663
|
-
@events << [:session_title, PWN::Sessions.refresh_title(session_id: session_id
|
|
663
|
+
@events << [:session_title, PWN::Sessions.refresh_title(session_id: session_id, ai_config: ai_config, on_error: lambda { |kind|
|
|
664
|
+
@events << [:warning, "Session title refresh unavailable for #{session_id} (#{kind}); previous title retained. Resume this session to retry."]
|
|
665
|
+
})]
|
|
664
666
|
ensure
|
|
665
667
|
@events << [:title_done, nil]
|
|
666
668
|
end
|
|
@@ -689,8 +691,9 @@ module PWN
|
|
|
689
691
|
PWN::Plugins::Log.mirror_tui!(msg: "#{type.to_s.upcase}\n#{clean}") if @pry.config.pwn_ai_debug && type != :notice
|
|
690
692
|
end
|
|
691
693
|
|
|
692
|
-
def start_request(request, command: false)
|
|
694
|
+
def start_request(request, command: false, &operation)
|
|
693
695
|
session_id = @pry.config.pwn_ai_session_id
|
|
696
|
+
title_config = Marshal.load(Marshal.dump(PWN::Env[:ai]))
|
|
694
697
|
@locals = Thread.current.keys.select { |key| key.to_s.start_with?('pwn_') }.to_h { |key| [key, Thread.current[key]] }
|
|
695
698
|
@thread_variables = Thread.current.thread_variables.select { |key| key.to_s.start_with?('pwn_') }.to_h { |key| [key, Thread.current.thread_variable_get(key)] }
|
|
696
699
|
# A fresh forwarding pipe prevents unread input crossing requests.
|
|
@@ -725,16 +728,24 @@ module PWN
|
|
|
725
728
|
end
|
|
726
729
|
if command
|
|
727
730
|
control.checkpoint(messages: [], phase: :tool)
|
|
728
|
-
if
|
|
731
|
+
if operation
|
|
732
|
+
operation.call
|
|
733
|
+
elsif request.match?(%r{\A/model(?:\s|$)})
|
|
729
734
|
REPL.pwn_ai_run_model(args: request.split.drop(1), prompt: lambda { |selection|
|
|
730
735
|
control.checkpoint(messages: [])
|
|
731
736
|
@events << [:model_prompt, selection]
|
|
732
737
|
:deferred
|
|
733
738
|
})
|
|
734
739
|
else
|
|
735
|
-
REPL.pwn_ai_dispatch_slash!(request: request, pry: @pry)
|
|
740
|
+
REPL.pwn_ai_dispatch_slash!(request: request, pry: @pry, raise_errors: true)
|
|
741
|
+
end
|
|
742
|
+
# Maintenance is one safe transaction: never label its committed
|
|
743
|
+
# deletions cancelled merely because stop was requested mid-write.
|
|
744
|
+
if request.match?(%r{\A/sessions\s+cleanup(?:\s|$)})
|
|
745
|
+
@events << [:notice, 'Session cleanup finished; completed work is not undone.']
|
|
746
|
+
else
|
|
747
|
+
control.checkpoint(messages: [])
|
|
736
748
|
end
|
|
737
|
-
control.checkpoint(messages: [])
|
|
738
749
|
else
|
|
739
750
|
workspace = File.realpath(Dir.pwd)
|
|
740
751
|
workspace_lock = File.open(File.join(Dir.tmpdir, "pwn-solve-#{Digest::SHA256.hexdigest(workspace)}.lock"), File::RDWR | File::CREAT | File::NOFOLLOW, 0o600) # rubocop:disable Style/FileOpen -- worker ensure owns the lease
|
|
@@ -757,7 +768,7 @@ module PWN
|
|
|
757
768
|
@events << [:warning, "#{e.class}: #{e.message}"]
|
|
758
769
|
ensure
|
|
759
770
|
workspace_lock&.close
|
|
760
|
-
@events << [:done, command ? nil : session_id]
|
|
771
|
+
@events << [:done, command ? nil : [session_id, title_config]]
|
|
761
772
|
end
|
|
762
773
|
@worker.report_on_exception = false
|
|
763
774
|
@control = ready.pop
|
|
@@ -810,7 +821,7 @@ module PWN
|
|
|
810
821
|
clear_view
|
|
811
822
|
elsif ['/sessions', '/sessions picker'].include?(line.strip)
|
|
812
823
|
open_operator(:sessions)
|
|
813
|
-
elsif line.strip == '/sessions cleanup'
|
|
824
|
+
elsif line.strip == '/sessions cleanup --dry-run'
|
|
814
825
|
open_operator(:cleanup)
|
|
815
826
|
elsif line.strip == '/evidence'
|
|
816
827
|
open_operator(:evidence)
|
|
@@ -837,7 +848,7 @@ module PWN
|
|
|
837
848
|
open_vault_edit(line)
|
|
838
849
|
elsif line.strip == '/system-role'
|
|
839
850
|
open_system_role
|
|
840
|
-
elsif line.match?(%r{\A/(?:mcp(?:\s|$)|cron\s+run(?:\s|$)|model(?:\s|$))})
|
|
851
|
+
elsif line.match?(%r{\A/(?:mcp(?:\s|$)|cron\s+run(?:\s|$)|model(?:\s|$)|sessions\s+cleanup(?:\s|$))})
|
|
841
852
|
start_request(line, command: true)
|
|
842
853
|
elsif line.start_with?('/') || line.start_with?('ai.profile ', 'ai.memory ')
|
|
843
854
|
if line.match?(%r{\A/sessions[[:space:]]+resume(?:[[:space:]]|$)})
|
|
@@ -845,6 +856,7 @@ module PWN
|
|
|
845
856
|
|
|
846
857
|
REPL.pwn_ai_dispatch_slash!(request: line, pry: @pry)
|
|
847
858
|
@session_metadata = PWN::Sessions.metadata(session_id: @pry.config.pwn_ai_session_id)
|
|
859
|
+
queue_title_update(@pry.config.pwn_ai_session_id)
|
|
848
860
|
elsif !REPL.pwn_ai_dispatch_slash!(request: line, pry: @pry)
|
|
849
861
|
add(:warning, 'Unknown command. Use / or Tab for commands.')
|
|
850
862
|
end
|
|
@@ -2027,7 +2039,7 @@ module PWN
|
|
|
2027
2039
|
view[:rows] = PWN::Sessions.list.sort_by { |row| [row[:mtime].to_s, row[:id]] } if view[:kind] == :sessions
|
|
2028
2040
|
if view[:kind] == :cleanup
|
|
2029
2041
|
require File.expand_path('../../../../etc/default_cron/session_cleanup', __dir__)
|
|
2030
|
-
view[:plan] = PWN::SessionCleanup.
|
|
2042
|
+
view[:plan] = PWN::SessionCleanup.maintain(dry_run: true, current_session_id: session_id)
|
|
2031
2043
|
view[:rows] = view[:plan][:records].map do |row|
|
|
2032
2044
|
category = row[:skip] || row[:apply_blocked_by] || (if row[:classification] == :named
|
|
2033
2045
|
:summarized
|
|
@@ -2352,7 +2364,7 @@ module PWN
|
|
|
2352
2364
|
['START HERE · local guide; no work is launched here.',
|
|
2353
2365
|
'Write a task in MISSION CONTROL. /readiness checks routes, tools and workspace locally.',
|
|
2354
2366
|
'Ctrl+S opens the team workspace. v previews your exact mission draft before confirmation.',
|
|
2355
|
-
'/evidence inspects actual checks and revisions. /sessions
|
|
2367
|
+
'/evidence inspects actual checks and revisions. /sessions cleanup runs maintenance; add --dry-run to preview.',
|
|
2356
2368
|
'/learning inbox reviews retained evidence. /steer queues guidance; Ctrl+C requests cancellation, not undo.',
|
|
2357
2369
|
'1 Insert: Inspect this workspace and explain its entry points without changing files.',
|
|
2358
2370
|
'2 Insert: Add a regression test for the reported bug, fix it, and execute the test.',
|
|
@@ -2390,13 +2402,17 @@ module PWN
|
|
|
2390
2402
|
end
|
|
2391
2403
|
|
|
2392
2404
|
def apply_operator_cleanup
|
|
2405
|
+
return add(:warning, 'Maintenance waits until the active request or swarm finishes.') if busy?
|
|
2406
|
+
|
|
2393
2407
|
view = @operator_view
|
|
2394
|
-
approved = view[:plan][:records].select { |row| view[:plan][:candidate_ids].include?(row[:id]) }.to_h { |row| [row[:id], row[:files]] }
|
|
2395
|
-
|
|
2408
|
+
approved = view[:plan][:records].select { |row| view[:plan][:candidate_ids].include?(row[:id]) }.to_h { |row| [row[:id], { files: row[:files], dependency_records: row[:dependency_records] }] }
|
|
2409
|
+
session_id = @pry.config.pwn_ai_session_id
|
|
2396
2410
|
view.delete(:confirm)
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2411
|
+
@operator_view = nil
|
|
2412
|
+
start_request('/sessions cleanup (approved preview)', command: true) do
|
|
2413
|
+
result = PWN::SessionCleanup.run(dry_run: false, current_session_id: session_id, approved_candidates: approved)
|
|
2414
|
+
@events << [:command, REPL.pwn_ai_result_text(value: result)]
|
|
2415
|
+
end
|
|
2400
2416
|
end
|
|
2401
2417
|
|
|
2402
2418
|
def operator_viewport(page)
|
data/lib/pwn/session_trace.rb
CHANGED
|
@@ -119,7 +119,7 @@ module PWN
|
|
|
119
119
|
id = opts[:session_id].to_s
|
|
120
120
|
raise ArgumentError, 'Invalid session id' unless id.match?(/\A[A-Za-z0-9][A-Za-z0-9_-]{0,127}\z/)
|
|
121
121
|
|
|
122
|
-
dir = File.
|
|
122
|
+
dir = File.dirname(PWN::Sessions.support_path(kind: :trace, session_id: id))
|
|
123
123
|
raise ArgumentError, 'Symlinked trace directory' if File.symlink?(dir)
|
|
124
124
|
|
|
125
125
|
File.join(dir, 'trace.jsonl')
|