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.
- checksums.yaml +4 -4
- data/lib/pwn/ai/agent/curriculum.rb +12 -4
- data/lib/pwn/ai/agent/learning.rb +443 -14
- data/lib/pwn/ai/agent/loop.rb +25 -5
- data/lib/pwn/ai/agent/mistakes.rb +124 -6
- data/lib/pwn/ai/agent/tools/learning.rb +22 -0
- data/lib/pwn/ai/agent/tools/memory.rb +29 -4
- data/lib/pwn/ai/agent/tools/mistakes.rb +22 -0
- data/lib/pwn/ai/agent/tools/sessions.rb +37 -2
- data/lib/pwn/memory.rb +105 -6
- data/lib/pwn/plugins/repl.rb +2 -2
- data/lib/pwn/sessions.rb +268 -4
- data/lib/pwn/version.rb +1 -1
- data/spec/integration/persistence_roundtrip_spec.rb +2 -2
- data/spec/lib/pwn/ai/agent/lean_stores_spec.rb +269 -0
- data/spec/lib/pwn/ai/agent/learning_spec.rb +137 -0
- data/spec/lib/pwn/ai/agent/loop_spec.rb +42 -0
- data/spec/lib/pwn/ai/agent/mistakes_spec.rb +75 -0
- data/spec/lib/pwn/memory_spec.rb +46 -1
- data/spec/lib/pwn/sessions_spec.rb +40 -1
- data/third_party/pwn_rdoc.jsonl +21 -3
- metadata +2 -1
|
@@ -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,
|
|
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[:
|
|
190
|
-
m[:
|
|
191
|
-
m[:
|
|
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,
|
|
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: {
|
|
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
|
-
{
|
|
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
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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
|
data/lib/pwn/plugins/repl.rb
CHANGED
|
@@ -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
|