pwn 0.5.663 → 0.5.665

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.
@@ -35,6 +35,14 @@ module PWN
35
35
  module Mistakes
36
36
  MISTAKES_FILE = File.join(Dir.home, '.pwn', 'mistakes.json')
37
37
  REPEAT_THRESHOLD = 3
38
+ # Lean retention for mistakes.json
39
+ SAMPLE_ARGS_MAX = 160
40
+ SNIPPET_MAX = 160
41
+ ERROR_MAX = 300
42
+ SESSIONS_KEEP = 3
43
+ MAX_RESOLVED_KEPT = 80
44
+ RESOLVED_MIN_AGE_DAYS = 21
45
+ FIX_MAX = 500
38
46
 
39
47
  CORRECTION_RX = /
40
48
  \b(
@@ -169,8 +177,8 @@ module PWN
169
177
  norm = normalize_error(error: error)
170
178
 
171
179
  m = store[key] ||= {
172
- signature: sig, tool: tool, error: norm,
173
- snippet: error.to_s.strip[0, 300],
180
+ signature: sig, tool: tool, error: norm.to_s[0, ERROR_MAX],
181
+ snippet: error.to_s.strip[0, SNIPPET_MAX],
174
182
  count: 0, drift_count: 0, first_seen: now, sessions: [],
175
183
  resolved: false, fix: nil, source: (opts[:source] || :tool).to_s
176
184
  }
@@ -186,9 +194,10 @@ module PWN
186
194
  m[:count] += 1
187
195
  end
188
196
  m[:last_seen] = now
189
- m[:snippet] = error.to_s.strip[0, 300]
190
- m[:sample_args] = opts[:args].to_s[0, 200] if opts[:args]
191
- m[:sessions] = (Array(m[:sessions]) + [opts[:session_id]]).compact.uniq.last(10)
197
+ m[:error] = norm.to_s[0, ERROR_MAX]
198
+ m[:snippet] = error.to_s.strip[0, SNIPPET_MAX]
199
+ m[:sample_args] = opts[:args].to_s[0, SAMPLE_ARGS_MAX] if opts[:args]
200
+ m[:sessions] = (Array(m[:sessions]) + [opts[:session_id]]).compact.uniq.last(SESSIONS_KEEP)
192
201
  # 2.2 — recoverable shape for repair routing
193
202
  if opts[:shape]
194
203
  m[:shape] = opts[:shape].to_s
@@ -229,7 +238,7 @@ module PWN
229
238
 
230
239
  store[key][:resolved] = true
231
240
  store[key][:regressed] = false
232
- store[key][:fix] = fix.strip[0, 500]
241
+ store[key][:fix] = fix.strip[0, FIX_MAX]
233
242
  store[key][:resolved_at] = Time.now.utc.iso8601
234
243
  # 2.3 — structured fix payload (strategy/tool/args_template/holdouts).
235
244
  # Prose-only resolve is why shell sigs regressed after auto-curriculum.
@@ -473,6 +482,115 @@ module PWN
473
482
  nil
474
483
  end
475
484
 
485
+ # Supported Method Parameters::
486
+ # result = PWN::AI::Agent::Mistakes.lean!(
487
+ # dry_run: 'optional - Boolean (default false)',
488
+ # max_resolved_kept: 'optional - cap on resolved-with-fix records',
489
+ # resolved_min_age_days: 'optional - age before resolved count=1 may drop'
490
+ # )
491
+ #
492
+ # Compact text fields on every record. Never drops unresolved,
493
+ # regressed, or high-count repeaters. Aged resolved-once fixes may
494
+ # drop after Memory already holds mistake_fix_<sig>.
495
+
496
+ public_class_method def self.lean!(opts = {})
497
+ dry = opts[:dry_run] ? true : false
498
+ max_resolved = (opts[:max_resolved_kept] || MAX_RESOLVED_KEPT).to_i
499
+ min_age = (opts[:resolved_min_age_days] || RESOLVED_MIN_AGE_DAYS).to_f
500
+ store = load
501
+ before_bytes = File.exist?(MISTAKES_FILE) ? File.size(MISTAKES_FILE) : 0
502
+ now = Time.now.utc
503
+ compacted = 0
504
+ dropped = []
505
+
506
+ store.each_value do |m|
507
+ before = begin
508
+ m.to_json.bytesize
509
+ rescue StandardError
510
+ 0
511
+ end
512
+ m[:sample_args] = m[:sample_args].to_s[0, SAMPLE_ARGS_MAX] if m[:sample_args].to_s.bytesize > SAMPLE_ARGS_MAX
513
+ m[:snippet] = m[:snippet].to_s[0, SNIPPET_MAX] if m[:snippet].to_s.bytesize > SNIPPET_MAX
514
+ m[:error] = m[:error].to_s[0, ERROR_MAX] if m[:error].to_s.bytesize > ERROR_MAX
515
+ m[:sessions] = Array(m[:sessions]).compact.uniq.last(SESSIONS_KEEP)
516
+ after = begin
517
+ m.to_json.bytesize
518
+ rescue StandardError
519
+ before
520
+ end
521
+ compacted += 1 if after < before
522
+ end
523
+
524
+ age_days = lambda do |m|
525
+ t = m[:resolved_at] || m[:last_seen]
526
+ (now - Time.parse(t.to_s)) / 86_400.0
527
+ rescue StandardError
528
+ 0.0
529
+ end
530
+
531
+ protected_m = lambda do |m|
532
+ return true unless m[:resolved]
533
+ return true if m[:regressed]
534
+ return true if effective_count(mistake: m) >= REPEAT_THRESHOLD
535
+ return true if m[:count].to_i >= 2 && m[:fix].to_s.strip != ''
536
+ return true if m[:fix].to_s.strip != '' && age_days.call(m) < min_age
537
+
538
+ false
539
+ end
540
+
541
+ # Drop aged resolved-once when fix lives in Memory or past age
542
+ store.each do |sig, m|
543
+ next if protected_m.call(m)
544
+ next unless m[:resolved] && m[:fix].to_s.strip != '' && m[:count].to_i <= 1
545
+ next unless age_days.call(m) >= min_age
546
+
547
+ mem_has = false
548
+ if defined?(PWN::Memory)
549
+ begin
550
+ mem_has = PWN::Memory.load.key?(:"mistake_fix_#{sig}")
551
+ rescue StandardError
552
+ mem_has = false
553
+ end
554
+ end
555
+ # Drop when Memory has the fix OR age is well past (2× min) even without mem key
556
+ dropped << sig.to_s if mem_has || age_days.call(m) >= (min_age * 2)
557
+ end
558
+
559
+ dropped.each { |s| store.delete(s.to_sym) } unless dry
560
+
561
+ # Cap resolved-with-fix by oldest resolved_at
562
+ resolved = store.select { |_s, m| m[:resolved] && m[:fix].to_s.strip != '' }
563
+ if resolved.size > max_resolved
564
+ excess = resolved.sort_by { |_s, m| m[:resolved_at].to_s }
565
+ .first(resolved.size - max_resolved)
566
+ excess.each do |sig, m|
567
+ next if m[:regressed] || m[:count].to_i >= 2
568
+ next if effective_count(mistake: m) >= REPEAT_THRESHOLD
569
+
570
+ dropped << sig.to_s
571
+ store.delete(sig) unless dry
572
+ end
573
+ end
574
+
575
+ save(store: store) unless dry
576
+
577
+ open_n = store.values.count { |m| !m[:resolved] }
578
+ {
579
+ compacted_fields: compacted,
580
+ dropped: dropped.uniq.length,
581
+ dropped_sigs: dropped.uniq.first(20),
582
+ remaining: store.size,
583
+ unresolved: open_n,
584
+ bytes_before: before_bytes,
585
+ bytes_after: if dry
586
+ before_bytes
587
+ else
588
+ (File.exist?(MISTAKES_FILE) ? File.size(MISTAKES_FILE) : 0)
589
+ end,
590
+ dry_run: dry
591
+ }
592
+ end
593
+
476
594
  # Supported Method Parameters::
477
595
  # PWN::AI::Agent::Mistakes.reset
478
596
 
@@ -219,3 +219,25 @@ PWN::AI::Agent::Registry.register(
219
219
  { previous: prev, current: ai[:agent][:auto_introspect] ? true : false }
220
220
  }
221
221
  )
222
+
223
+ PWN::AI::Agent::Registry.register(
224
+ name: 'learning_gc_stores',
225
+ toolset: 'learning',
226
+ schema: {
227
+ name: 'learning_gc_stores',
228
+ description: 'Lean-keep ~/.pwn RL stores (memory, learning.jsonl, mistakes.json, ' \
229
+ 'sessions/) without dropping gold outcomes, open mistakes, prefs/SOPs, ' \
230
+ 'or reference-pinned session transcripts. dry_run:true plans only.',
231
+ parameters: {
232
+ type: 'object',
233
+ properties: {
234
+ dry_run: { type: 'boolean', default: false, description: 'Report only; do not rewrite.' }
235
+ },
236
+ required: []
237
+ }
238
+ },
239
+ check: -> { defined?(PWN::AI::Agent::Learning) && PWN::AI::Agent::Learning.respond_to?(:gc_stores!) },
240
+ handler: lambda { |args|
241
+ PWN::AI::Agent::Learning.gc_stores!(dry_run: args[:dry_run] ? true : false)
242
+ }
243
+ )
@@ -60,16 +60,19 @@ PWN::AI::Agent::Registry.register(
60
60
  toolset: 'memory',
61
61
  schema: {
62
62
  name: 'memory_forget',
63
- description: 'Delete a persistent memory entry by key.',
63
+ description: 'Delete a persistent memory entry by key. Protected keys (operator_pref_*, process_sop_*, mistake_fix_*, memory_*, category preference) require force:true.',
64
64
  parameters: {
65
65
  type: 'object',
66
- properties: { key: { type: 'string' } },
66
+ properties: {
67
+ key: { type: 'string' },
68
+ force: { type: 'boolean', description: 'Bypass protect policy (default false).' }
69
+ },
67
70
  required: %w[key]
68
71
  }
69
72
  },
70
73
  check: -> { defined?(PWN::Memory) },
71
74
  handler: lambda { |args|
72
- PWN::Memory.forget(key: args[:key].to_s.to_sym)
75
+ PWN::Memory.forget(key: args[:key].to_s.to_sym, force: args[:force] == true)
73
76
  { forgotten: args[:key] }
74
77
  }
75
78
  )
@@ -96,7 +99,29 @@ PWN::AI::Agent::Registry.register(
96
99
  raise ArgumentError, 'refusing to clear memory without confirm:true' unless args[:confirm] == true
97
100
 
98
101
  before = PWN::Memory.load.keys.length
99
- PWN::Memory.clear
102
+ PWN::Memory.clear(force: true)
100
103
  { cleared: true, entries_removed: before, file: PWN::Memory::MEMORY_FILE }
101
104
  }
102
105
  )
106
+
107
+ PWN::AI::Agent::Registry.register(
108
+ name: 'memory_lean',
109
+ toolset: 'memory',
110
+ schema: {
111
+ name: 'memory_lean',
112
+ description: 'Drop expired ephemeral session_* memory keys and truncate overlong ' \
113
+ 'values. Never removes operator prefs, process SOPs, or mistake_fix_* ' \
114
+ 'lessons. dry_run:true plans only.',
115
+ parameters: {
116
+ type: 'object',
117
+ properties: {
118
+ dry_run: { type: 'boolean', default: false }
119
+ },
120
+ required: []
121
+ }
122
+ },
123
+ check: -> { defined?(PWN::Memory) && PWN::Memory.respond_to?(:lean!) },
124
+ handler: lambda { |args|
125
+ PWN::Memory.lean!(dry_run: args[:dry_run] ? true : false)
126
+ }
127
+ )
@@ -104,6 +104,28 @@ PWN::AI::Agent::Registry.register(
104
104
  }
105
105
  )
106
106
 
107
+ PWN::AI::Agent::Registry.register(
108
+ name: 'mistakes_lean',
109
+ toolset: 'learning',
110
+ schema: {
111
+ name: 'mistakes_lean',
112
+ description: 'Compact mistakes.json fields and age-out resolved-once signatures ' \
113
+ 'after fixes live in Memory. Never drops unresolved or regressed. ' \
114
+ 'dry_run:true plans only.',
115
+ parameters: {
116
+ type: 'object',
117
+ properties: {
118
+ dry_run: { type: 'boolean', default: false }
119
+ },
120
+ required: []
121
+ }
122
+ },
123
+ check: -> { defined?(PWN::AI::Agent::Mistakes) && PWN::AI::Agent::Mistakes.respond_to?(:lean!) },
124
+ handler: lambda { |args|
125
+ PWN::AI::Agent::Mistakes.lean!(dry_run: args[:dry_run] ? true : false)
126
+ }
127
+ )
128
+
107
129
  PWN::AI::Agent::Registry.register(
108
130
  name: 'mistakes_reset',
109
131
  toolset: 'learning',
@@ -80,7 +80,11 @@ PWN::AI::Agent::Registry.register(
80
80
  parameters: {
81
81
  type: 'object',
82
82
  properties: {
83
- session_id: { type: 'string' }
83
+ session_id: { type: 'string' },
84
+ force: {
85
+ type: 'boolean',
86
+ description: 'Bypass pin policy for hot/gold/mistake sessions (default false).'
87
+ }
84
88
  },
85
89
  required: %w[session_id]
86
90
  }
@@ -90,7 +94,10 @@ PWN::AI::Agent::Registry.register(
90
94
  sid = args[:session_id].to_s
91
95
  raise ArgumentError, 'session_id is required' if sid.empty?
92
96
 
93
- { session_id: sid, deleted: PWN::Sessions.delete(session_id: sid) }
97
+ {
98
+ session_id: sid,
99
+ deleted: PWN::Sessions.delete(session_id: sid, force: args[:force] == true)
100
+ }
94
101
  }
95
102
  )
96
103
 
@@ -137,3 +144,31 @@ PWN::AI::Agent::Registry.register(
137
144
  }
138
145
  }
139
146
  )
147
+
148
+ PWN::AI::Agent::Registry.register(
149
+ name: 'sessions_lean',
150
+ toolset: 'sessions',
151
+ schema: {
152
+ name: 'sessions_lean',
153
+ description: 'Prune/compact ~/.pwn/sessions: delete stubs and unreferenced aged ' \
154
+ 'transcripts; compact oversized tool/assistant content. Pins gold ' \
155
+ 'learning outcomes and open-mistake session refs. dry_run:true plans only.',
156
+ parameters: {
157
+ type: 'object',
158
+ properties: {
159
+ dry_run: { type: 'boolean', default: false },
160
+ retain_days: { type: 'integer', default: 30 },
161
+ max_files: { type: 'integer', default: 400 }
162
+ },
163
+ required: []
164
+ }
165
+ },
166
+ check: -> { defined?(PWN::Sessions) && PWN::Sessions.respond_to?(:lean!) },
167
+ handler: lambda { |args|
168
+ PWN::Sessions.lean!(
169
+ dry_run: args[:dry_run] ? true : false,
170
+ retain_days: args[:retain_days],
171
+ max_files: args[:max_files]
172
+ )
173
+ }
174
+ )
data/lib/pwn/memory.rb CHANGED
@@ -15,6 +15,12 @@ module PWN
15
15
  # via ruby code blocks during execution loops.
16
16
  module Memory
17
17
  MEMORY_FILE = File.join(Dir.home, '.pwn', 'memory.json')
18
+ # Lean retention — keep RL-quality signal, drop ephemeral bulk.
19
+ VALUE_MAX_CHARS = 2_000
20
+ PROTECT_KEY_PREFIXES = %w[operator_pref_ process_sop_ mistake_fix_ memory_].freeze
21
+ PROTECT_CATEGORIES = %i[preference].freeze
22
+ EPHEMERAL_KEY_PREFIXES = %w[session_].freeze
23
+ EPHEMERAL_TTL_SECS = 7 * 86_400
18
24
 
19
25
  # Supported Method Parameters::
20
26
  # memory = PWN::Memory.load
@@ -51,7 +57,7 @@ module PWN
51
57
  warn "[pwn-ai/memory] refusing empty overwrite of #{File.size(path)}B #{path} (pass force:true to clear)"
52
58
  return load_raw_or_empty(path: path)
53
59
  end
54
- tmp = File.join(File.dirname(path), ".#{File.basename(path)}.#{Process.pid}.tmp")
60
+ tmp = File.join(File.dirname(path), ".#{File.basename(path)}.#{Process.pid}.tmp")
55
61
  body = JSON.pretty_generate(mem)
56
62
  File.open(tmp, File::WRONLY | File::CREAT | File::TRUNC, 0o644) do |f|
57
63
  f.flock(File::LOCK_EX)
@@ -93,8 +99,10 @@ module PWN
93
99
  raise 'ERROR: key and value are required' if key.nil? || value.nil?
94
100
 
95
101
  mem = load
102
+ val = value.is_a?(String) ? value.to_s : value
103
+ val = "#{val.to_s[0, VALUE_MAX_CHARS]}…[compacted]" if val.is_a?(String) && val.bytesize > VALUE_MAX_CHARS
96
104
  entry = {
97
- value: value,
105
+ value: val,
98
106
  category: category.to_sym,
99
107
  timestamp: Time.now.utc.iso8601,
100
108
  # M3 — provenance & scoring so Learning.consolidate evicts by
@@ -147,10 +155,20 @@ module PWN
147
155
  end
148
156
 
149
157
  # Supported Method Parameters::
150
- # PWN::Memory.forget(key: :some_key)
158
+ # PWN::Memory.forget(
159
+ # key: :some_key,
160
+ # force: 'optional - Boolean bypass protect policy (default false)'
161
+ # )
162
+ # Refuses PROTECT_KEY_PREFIXES / PROTECT_CATEGORIES unless force:true.
151
163
  public_class_method def self.forget(opts = {}) # rubocop:disable Naming/PredicateMethod
152
164
  key = opts[:key]
165
+ raise 'ERROR: key is required' if key.nil?
166
+
167
+ force = opts[:force] ? true : false
153
168
  mem = load
169
+ entry = mem[key.to_sym]
170
+ raise "ERROR: refusing to forget protected memory key #{key.inspect} (matches PROTECT_KEY_PREFIXES/PROTECT_CATEGORIES; pass force:true)" if entry && protected_entry?(key: key, entry: entry) && !force
171
+
154
172
  mem.delete(key.to_sym)
155
173
  # Last-key delete legitimately yields {}; force so empty-guard does not revive it.
156
174
  save(mem: mem, force: mem.empty?)
@@ -158,8 +176,16 @@ module PWN
158
176
  end
159
177
 
160
178
  # Supported Method Parameters::
161
- # PWN::Memory.clear
162
- public_class_method def self.clear
179
+ # PWN::Memory.clear(force: true)
180
+ # Requires force:true — protected prefs/SOPs must not vanish via bare clear.
181
+ public_class_method def self.clear(opts = {})
182
+ force = if opts.is_a?(Hash)
183
+ opts[:force] ? true : false
184
+ else
185
+ false
186
+ end
187
+ raise 'ERROR: refusing Memory.clear without force:true (would drop PROTECT_KEY_PREFIXES/PROTECT_CATEGORIES)' unless force
188
+
163
189
  FileUtils.rm_f(MEMORY_FILE)
164
190
  save(mem: {}, force: true) # recreate empty file atomically
165
191
  {}
@@ -180,6 +206,77 @@ module PWN
180
206
  ctx
181
207
  end
182
208
 
209
+ # True when a memory key must survive cap eviction / age GC.
210
+ public_class_method def self.protected_entry?(opts = {})
211
+ key = opts[:key].to_s
212
+ entry = opts[:entry] || {}
213
+ return true if PROTECT_KEY_PREFIXES.any? { |p| key.start_with?(p) }
214
+ return true if PROTECT_CATEGORIES.map(&:to_s).include?(entry[:category].to_s)
215
+
216
+ false
217
+ end
218
+
219
+ # Supported Method Parameters::
220
+ # result = PWN::Memory.lean!(
221
+ # dry_run: 'optional - Boolean plan only (default false)',
222
+ # value_max_chars: 'optional - truncate values (default VALUE_MAX_CHARS)',
223
+ # ephemeral_ttl_secs: 'optional - drop expired session_* keys'
224
+ # )
225
+ #
226
+ # Compact overlong values and drop expired ephemeral session_* keys.
227
+ # Never removes protected prefs/SOPs/fix lessons. Safe with empty-save guard.
228
+ public_class_method def self.lean!(opts = {})
229
+ dry = opts[:dry_run] ? true : false
230
+ vmax = (opts[:value_max_chars] || VALUE_MAX_CHARS).to_i
231
+ ttl_secs = (opts[:ephemeral_ttl_secs] || EPHEMERAL_TTL_SECS).to_i
232
+ mem = load
233
+ before_bytes = File.exist?(MEMORY_FILE) ? File.size(MEMORY_FILE) : 0
234
+ removed = []
235
+ truncated = []
236
+ now = Time.now.utc
237
+
238
+ mem.each do |k, v|
239
+ key = k.to_s
240
+ next if protected_entry?(key: key, entry: v)
241
+
242
+ if EPHEMERAL_KEY_PREFIXES.any? { |p| key.start_with?(p) }
243
+ age = begin
244
+ now - Time.parse(v[:timestamp].to_s)
245
+ rescue StandardError
246
+ ttl_secs + 1
247
+ end
248
+ exp = v[:ttl].to_i.positive? ? v[:ttl].to_i : ttl_secs
249
+ if age > exp
250
+ removed << key
251
+ next
252
+ end
253
+ end
254
+
255
+ next unless v[:value].is_a?(String) && v[:value].to_s.bytesize > vmax
256
+
257
+ truncated << key
258
+ v[:value] = "#{v[:value].to_s[0, vmax]}…[compacted]" unless dry
259
+ end
260
+
261
+ removed.each { |k| mem.delete(k.to_sym) } unless dry
262
+ save(mem: mem, force: mem.empty?) unless dry || (removed.empty? && truncated.empty?)
263
+
264
+ {
265
+ removed: removed.length,
266
+ truncated: truncated.length,
267
+ remaining: mem.size,
268
+ removed_keys: removed.first(20),
269
+ truncated_keys: truncated.first(20),
270
+ bytes_before: before_bytes,
271
+ bytes_after: if dry
272
+ before_bytes
273
+ else
274
+ (File.exist?(MEMORY_FILE) ? File.size(MEMORY_FILE) : 0)
275
+ end,
276
+ dry_run: dry
277
+ }
278
+ end
279
+
183
280
  # Author(s):: 0day Inc. <support@0dayinc.com>
184
281
 
185
282
  public_class_method def self.authors
@@ -195,8 +292,10 @@ module PWN
195
292
  facts = PWN::Memory.recall(query: 'recon', category: :fact, limit: 10)
196
293
  hits = PWN::Memory.recall_semantic(query: 'recon', limit: 6) # embedding-ranked
197
294
  PWN::Memory.forget(key: :some_key)
198
- PWN::Memory.clear
295
+ PWN::Memory.forget(key: :operator_pref_x) rescue puts('protected')
296
+ PWN::Memory.clear(force: true)
199
297
  context_str = PWN::Memory.to_context
298
+ PWN::Memory.lean!(dry_run: true) # drop expired session_* + truncate values
200
299
 
201
300
  #{self}.authors
202
301
  USAGE
@@ -472,7 +472,7 @@ module PWN
472
472
  PWN::Memory.forget(key: args[1])
473
473
  puts "Forgot #{args[1]}"
474
474
  when 'clear'
475
- PWN::Memory.clear
475
+ PWN::Memory.clear(force: true)
476
476
  puts 'Memory cleared'
477
477
  else
478
478
  puts PWN::Memory.help
@@ -493,7 +493,7 @@ module PWN
493
493
  hist = PWN::Sessions.to_response_history(session_id: sid)
494
494
  puts "Loaded session #{sid} with #{hist[:choices].size} entries (set manually into response_history if needed)"
495
495
  when 'delete'
496
- PWN::Sessions.delete(session_id: args[1])
496
+ PWN::Sessions.delete(session_id: args[1], force: true)
497
497
  puts "Deleted #{args[1]}"
498
498
  when 'stats'
499
499
  puts PWN::Sessions.stats