software-defence-factory 0.15.4 → 0.15.5
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.
- package/docs/npm.md +10 -0
- package/docs/recovery.md +16 -0
- package/docs/setup.md +34 -0
- package/factory/execution-profile.mjs +5 -3
- package/factory/pi-context-budget.mjs +30 -1
- package/factory/pi-context-extension.mjs +103 -35
- package/factory/pi-local-launch.mjs +14 -1
- package/package.json +2 -2
package/docs/npm.md
CHANGED
|
@@ -282,3 +282,13 @@ reject tampered selections/images/policies and unsupported future writers, and
|
|
|
282
282
|
verify that readback does not rewrite retained evidence. Image adoption is a
|
|
283
283
|
separate idle operator action; existing attempt images are not migrated by a CLI
|
|
284
284
|
update. See [the migration/rollback recipe](setup.md#local-context-budget-and-worker-image-0152-100).
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
The 0.15.5 compatibility audit adds that exact writer to the explicit v1/v2/v3
|
|
288
|
+
allowlists. The examined change is confined to local Pi projection, proactive
|
|
289
|
+
recovery and completion validation. Evidence schemas, frozen commands, model and
|
|
290
|
+
image selections, policy hashes, candidate reconstruction and acceptance guards
|
|
291
|
+
remain unchanged from 0.15.4. Profile tests exercise all phases and inherited,
|
|
292
|
+
cloud, hybrid and local definitions, reject changed selections and future writers,
|
|
293
|
+
and verify immutable readback. This does not upgrade or qualify historical local
|
|
294
|
+
attempts; installed image adoption and actual inference remain separate gates.
|
package/docs/recovery.md
CHANGED
|
@@ -44,6 +44,22 @@ retry around a retained writer or reuse earlier story evidence for a changed
|
|
|
44
44
|
candidate or policy. Browser traces and screenshots are private evidence and
|
|
45
45
|
are presented through the shared interfaces. See [the web contract](web-verification.md).
|
|
46
46
|
|
|
47
|
+
## Local Pi history exhaustion
|
|
48
|
+
|
|
49
|
+
The 0.15.5 candidate adds bounded proactive recovery for short histories, including
|
|
50
|
+
productive writes whose immutable arguments require fewer retained groups;
|
|
51
|
+
see [limits and image adoption](setup.md#local-context-budget-and-worker-image-0152-100).
|
|
52
|
+
A failed/empty summary, exhausted recovery limit, cancellation or irreducible
|
|
53
|
+
request remains a failed attempt even if a tool already wrote a report. Keep its
|
|
54
|
+
checkout, raw events and private diagnostic evidence. Reconcile the stopped
|
|
55
|
+
worker before admitting a smaller task or explicitly selecting another qualified
|
|
56
|
+
profile as a separate attempt. Do not enlarge the allocation, rewrite a failed
|
|
57
|
+
history or label an explicit cloud retry as local success. Successful transport
|
|
58
|
+
recovery still needs semantic checks and independent Review. The reviewed first
|
|
59
|
+
checkpoint failed real local qualification; the revised artifact needs a new
|
|
60
|
+
lead-owned installed and real-task pass before acceptance. See the
|
|
61
|
+
[rejected trial and source regression](proof.md#short-local-pi-history-recovery-0155-candidate-105).
|
|
62
|
+
|
|
47
63
|
## Review feedback or phase retry
|
|
48
64
|
|
|
49
65
|
Use **Request changes** on a failed software review only when its validated
|
package/docs/setup.md
CHANGED
|
@@ -509,6 +509,40 @@ guessed or silently rewritten. Do not add project Pi settings to bypass
|
|
|
509
509
|
the admitted binding or rely on custom tuning that has not been qualified.
|
|
510
510
|
See [pinned compaction behavior](https://github.com/earendil-works/pi/blob/v0.87.1/packages/coding-agent/docs/compaction.md).
|
|
511
511
|
|
|
512
|
+
Starting with the **0.15.5 candidate (#105)**, the local adapter also checks
|
|
513
|
+
history pressure at Pi's completed-tool-turn boundary. This conservative byte
|
|
514
|
+
signal counts the system/task/tool envelope once, call arguments, non-tool text
|
|
515
|
+
and capped tool excerpts, separately from provider-reported usage. At 65% of the
|
|
516
|
+
input budget it considers Pi's supported summary generator and append-only
|
|
517
|
+
compaction entry. The newest complete tool group is retained; up to three older
|
|
518
|
+
complete groups can remain as additional evidence when the actual summary and
|
|
519
|
+
retained envelope fit. The original task is restored exactly on every generation;
|
|
520
|
+
raw events and existing session entries are not rewritten. Large-result bursts
|
|
521
|
+
still use upstream compaction and the final request projection. Projection gives
|
|
522
|
+
recent short results (up to 2 KiB each, 8 KiB total and at most a quarter of the
|
|
523
|
+
input budget) priority when they fit.
|
|
524
|
+
|
|
525
|
+
Recovery allows at most 16 proactive summaries per process and two retries per
|
|
526
|
+
summary. It must reduce pressure by at least 4 KiB and leave headroom of 8 KiB
|
|
527
|
+
(or 15% of the input budget when smaller). Its preferred reduction target adapts
|
|
528
|
+
to the immutable latest-group/envelope floor and actual summary size; it cannot
|
|
529
|
+
require that floor to fit an arbitrary fraction of the trigger. The next trigger
|
|
530
|
+
also leaves room for new work. If the latest write/group alone makes recovery
|
|
531
|
+
unproductive, the adapter defers to a later complete turn under the unchanged
|
|
532
|
+
wire guard, when that group can become summary input. This does not permit an
|
|
533
|
+
oversized request through transport. Empty, failed, cancelled or ineffective
|
|
534
|
+
summaries fail the attempt. Oversized instructions, arguments or retained groups
|
|
535
|
+
can still fail closed. These are bounded recovery limits, not automatic
|
|
536
|
+
context/output tuning or a guarantee of useful model work.
|
|
537
|
+
|
|
538
|
+
The selected context, generation output and model remain unchanged; summary
|
|
539
|
+
output is separately capped (6,553 for proactive summaries with 8,192 selected
|
|
540
|
+
output on pinned Pi). `factory_context_recovery` JSONL events record only
|
|
541
|
+
pressure, envelope/floor sizes, group counts and recovery progress, without
|
|
542
|
+
prompts/content. Pi token accounting stays partial; this summary path is not
|
|
543
|
+
added to ordinary assistant usage totals. See
|
|
544
|
+
[source measurements, rejected real trial and pending qualification](proof.md#short-local-pi-history-recovery-0155-candidate-105).
|
|
545
|
+
|
|
512
546
|
Before adoption, stop and reconcile all work, retain the old immutable image ID
|
|
513
547
|
and a stopped private-state backup. Build/qualify the exact candidate image, then
|
|
514
548
|
explicitly select it with `factory install --image LOCAL_IMAGE_REF --state PRIVATE_STATE`
|
|
@@ -29,16 +29,18 @@ import { expectedWebStories, webPolicyHash } from './web-verification.mjs';
|
|
|
29
29
|
// compatibility does not qualify an old local startup or accept a failed attempt.
|
|
30
30
|
// 0.15.4 adds Pi usage metadata only; protected evidence schemas, frozen
|
|
31
31
|
// execution selection, isolation and acceptance policy remain unchanged.
|
|
32
|
+
// 0.15.5 changes only local context recovery/projection; v1/v2/v3 schemas,
|
|
33
|
+
// frozen selection hashes, provenance, checks and acceptance guards are unchanged.
|
|
32
34
|
// Older writers still require exact patch/tree reconstruction before publication.
|
|
33
35
|
// Deliberately independent of VERSION: a release bump is not
|
|
34
36
|
// evidence compatibility. Re-audit this list for every trust-relevant writer,
|
|
35
37
|
// isolation or validation change; remove versions whose guarantees no longer
|
|
36
38
|
// satisfy current policy. See docs/npm.md. This predicate alone grants no trust.
|
|
37
|
-
const SUPPORTED_EXECUTION_RUNTIMES_V1 = new Set(['0.8.0', '0.9.0', '0.9.1', '0.10.0', '0.11.0', '0.11.1', '0.11.2', '0.12.0', '0.13.0', '0.13.1', '0.14.0', '0.15.0', '0.15.1', '0.15.2', '0.15.3', '0.15.4']);
|
|
39
|
+
const SUPPORTED_EXECUTION_RUNTIMES_V1 = new Set(['0.8.0', '0.9.0', '0.9.1', '0.10.0', '0.11.0', '0.11.1', '0.11.2', '0.12.0', '0.13.0', '0.13.1', '0.14.0', '0.15.0', '0.15.1', '0.15.2', '0.15.3', '0.15.4', '0.15.5']);
|
|
38
40
|
|
|
39
41
|
export function isSupportedExecutionProfile(profile) {
|
|
40
|
-
if ([2, 3].includes(profile?.version)) return (profile.version === 3 ? ['0.15.0', '0.15.1', '0.15.2', '0.15.3', '0.15.4'].includes(profile.runtimeVersion) && Object.hasOwn(profile, 'localBinding')
|
|
41
|
-
: ['0.14.0', '0.15.0', '0.15.1', '0.15.2', '0.15.3', '0.15.4'].includes(profile.runtimeVersion) && !Object.hasOwn(profile, 'localBinding') && profile.inferenceProvider !== 'factory-local')
|
|
42
|
+
if ([2, 3].includes(profile?.version)) return (profile.version === 3 ? ['0.15.0', '0.15.1', '0.15.2', '0.15.3', '0.15.4', '0.15.5'].includes(profile.runtimeVersion) && Object.hasOwn(profile, 'localBinding')
|
|
43
|
+
: ['0.14.0', '0.15.0', '0.15.1', '0.15.2', '0.15.3', '0.15.4', '0.15.5'].includes(profile.runtimeVersion) && !Object.hasOwn(profile, 'localBinding') && profile.inferenceProvider !== 'factory-local')
|
|
42
44
|
&& profile.role === (Object.keys(ROLE_PHASES).find(role => ROLE_PHASES[role] === profile.phase) || null)
|
|
43
45
|
&& (profile.reasoningEffort === null || ['low', 'medium', 'high'].includes(profile.reasoningEffort))
|
|
44
46
|
&& /^[a-f0-9]{64}$/.test(profile.selectionHash || '');
|
|
@@ -35,6 +35,16 @@ function excerpt(text, limit) {
|
|
|
35
35
|
return Buffer.byteLength(shortened) < Buffer.byteLength(text) ? shortened : text;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
// A pressure signal, not a token estimate: cap large tool bodies so upstream's
|
|
39
|
+
// existing large-result recovery still owns bursts, but count every call/argument
|
|
40
|
+
// and a useful short excerpt. Provider usage cannot measure this growing floor.
|
|
41
|
+
export function historyPressure(messages, envelope) {
|
|
42
|
+
const copy = structuredClone(messages);
|
|
43
|
+
for (const message of copy) if (message.role === 'toolResult')
|
|
44
|
+
for (const part of message.content || []) if (part.type === 'text') part.text = excerpt(part.text, 1024);
|
|
45
|
+
return envelope + bytes(copy) + 64 * copy.length;
|
|
46
|
+
}
|
|
47
|
+
|
|
38
48
|
// Uniform cap shares space among large results without penalizing small ones.
|
|
39
49
|
// Calls, arguments, identities, ordering and all non-tool text are immutable.
|
|
40
50
|
export function fitToolText(value, budget, measure = bytes, native = false) {
|
|
@@ -49,8 +59,27 @@ export function fitToolText(value, budget, measure = bytes, native = false) {
|
|
|
49
59
|
for (const part of message.content) if (part.type === 'text') slots.push([part, 'text', part.text]);
|
|
50
60
|
}
|
|
51
61
|
}
|
|
52
|
-
|
|
62
|
+
// Prefer recent short discoveries within a bounded allowance; large batches
|
|
63
|
+
// still share the remaining space. Prefix stability is measured separately.
|
|
64
|
+
const recent = new Set();
|
|
65
|
+
let recentBytes = 0;
|
|
66
|
+
for (const slot of [...slots].reverse()) {
|
|
67
|
+
const size = bytes(slot[2]);
|
|
68
|
+
if (size <= 2048 && recentBytes + size <= Math.min(8192, budget / 4)) {
|
|
69
|
+
recent.add(slot); recentBytes += size;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const cap = limit => { for (const slot of slots) {
|
|
73
|
+
const [object, key, text] = slot;
|
|
74
|
+
object[key] = recent.has(slot) ? text : excerpt(text, limit);
|
|
75
|
+
} };
|
|
53
76
|
cap(0);
|
|
77
|
+
// Release older preferences first if the complete preferred set cannot fit.
|
|
78
|
+
// Even one useful recent result is better than uniformly shortening them all.
|
|
79
|
+
for (const slot of [...recent].reverse()) {
|
|
80
|
+
if (measure(original) <= budget) break;
|
|
81
|
+
recent.delete(slot); cap(0);
|
|
82
|
+
}
|
|
54
83
|
if (measure(original) > budget) throw new Error('Local Pi context budget cannot fit required instructions, tool pairs and truncation guidance; use smaller task/input or qualify the configured allocation. No fallback.');
|
|
55
84
|
let low = 0, high = Math.max(0, ...slots.map(([, , text]) => text.length));
|
|
56
85
|
while (low < high) {
|
|
@@ -1,19 +1,87 @@
|
|
|
1
1
|
// Explicitly mounted Factory adapter, never discovered from the host/repository.
|
|
2
2
|
import { readFileSync, writeSync } from 'node:fs';
|
|
3
3
|
import { openAICompletionsApi } from '@earendil-works/pi-ai';
|
|
4
|
-
import {
|
|
4
|
+
import { generateSummaryWithUsage } from '@earendil-works/pi-coding-agent';
|
|
5
|
+
import { historyPressure, bytes, fitToolText, projectedTokens, restoreOutputBudget, validatePairs } from './pi-context-budget.mjs';
|
|
5
6
|
|
|
6
7
|
export default function factoryLocalContext(pi) {
|
|
7
8
|
const registry = JSON.parse(readFileSync(new URL('./models.json', import.meta.url), 'utf8'));
|
|
8
9
|
const configured = registry.providers['factory-local'].models[0];
|
|
9
10
|
const window = configured.contextWindow, output = configured.maxTokens;
|
|
10
|
-
let task;
|
|
11
|
+
let task, taskEntryId, envelope = 0, recoveries = 0, nextRecovery = 0;
|
|
11
12
|
// Pi catches extension exceptions and can continue. Budget violations must
|
|
12
13
|
// stop before transport, even during summarization, with a durable diagnostic.
|
|
13
14
|
const fatal = error => {
|
|
14
15
|
writeSync(2, `Factory local Pi context protection failed: ${error.message}\n`);
|
|
15
16
|
process.exit(1);
|
|
16
17
|
};
|
|
18
|
+
// Pi 0.87.1's public manual compact() aborts the active run. Use its
|
|
19
|
+
// supported completed-turn compaction draft instead: all tools have finished,
|
|
20
|
+
// and the next request rebuilds the canonical projection from this entry.
|
|
21
|
+
pi.on('turn_end', async (event, ctx) => {
|
|
22
|
+
if (event.outcome !== 'completed' || event.message.stopReason !== 'toolUse') return;
|
|
23
|
+
try {
|
|
24
|
+
const entries = event.context.contextEntries;
|
|
25
|
+
taskEntryId ??= entries.find(entry => entry.messages.some(m => m.role === 'user'))?.sourceEntry.id;
|
|
26
|
+
// Pi includes the system message in its boundary projection. The wire
|
|
27
|
+
// envelope already counts it and the restored task: count each once and
|
|
28
|
+
// use the same basis before/after, so removal cannot fake progress.
|
|
29
|
+
const history = list => list.filter(entry => entry.sourceEntry.id !== taskEntryId)
|
|
30
|
+
.flatMap(entry => entry.messages).filter(message => message.role !== 'system');
|
|
31
|
+
const budget = window - output;
|
|
32
|
+
const threshold = Math.max(Math.floor(budget * 0.65), nextRecovery);
|
|
33
|
+
const pressure = historyPressure(history(entries), envelope);
|
|
34
|
+
if (pressure < threshold) return;
|
|
35
|
+
const starts = entries.flatMap((entry, index) => entry.messages.some(m => m.role === 'assistant'
|
|
36
|
+
&& m.content?.some(p => p.type === 'toolCall')) ? [index] : []);
|
|
37
|
+
// The newest complete group is the retention floor, including immutable
|
|
38
|
+
// write arguments. Summarize the older prefix once; after seeing its real
|
|
39
|
+
// size we can retain up to four groups as additional recent evidence.
|
|
40
|
+
// Never split a group or summarize only part of a pending tool batch.
|
|
41
|
+
if (starts.length < 2) return;
|
|
42
|
+
const newest = starts.at(-1);
|
|
43
|
+
const floor = historyPressure(history(entries.slice(newest)), envelope);
|
|
44
|
+
const headroom = Math.min(8192, Math.floor(budget * 0.15));
|
|
45
|
+
// A large current call may leave no useful recovery yet. Let it reach the
|
|
46
|
+
// next ordinary turn under the unchanged wire guard, then it can enter
|
|
47
|
+
// the summarized prefix. Avoid paying for a predictably futile summary.
|
|
48
|
+
if (floor + 1024 > budget - headroom || pressure - floor < 4096) return;
|
|
49
|
+
if (++recoveries > 16) throw new Error('Local Pi proactive recovery limit reached; split the task. No fallback.');
|
|
50
|
+
if (ctx.signal?.aborted) throw new Error('Local Pi proactive recovery cancelled');
|
|
51
|
+
// Pi's summary serializer ignores system messages. Do not charge their
|
|
52
|
+
// bytes against the summary input projection; generation still keeps the
|
|
53
|
+
// exact system envelope. Keep the task and all summarized tool pairs.
|
|
54
|
+
const older = entries.slice(0, newest).flatMap(entry => entry.messages).filter(m => m.role !== 'system');
|
|
55
|
+
writeSync(1, `${JSON.stringify({ type: 'factory_context_recovery', phase: 'start', pressure, threshold, floor, envelope, groups: starts.length, recovery: recoveries })}\n`);
|
|
56
|
+
const input = fitToolText(older, window - output - 8192, bytes, true);
|
|
57
|
+
const result = await generateSummaryWithUsage(input, ctx.model, Math.min(16384, output),
|
|
58
|
+
'factory-local-keyless', undefined, ctx.signal,
|
|
59
|
+
'Preserve the task, verified edits, failed attempts, discoveries and remaining work. Do not claim completion without evidence.',
|
|
60
|
+
undefined, ctx.thinkingLevel, streamLocal, undefined, { enabled: true, maxRetries: 2, baseDelayMs: 250 });
|
|
61
|
+
if (ctx.signal?.aborted) throw new Error('Local Pi proactive recovery cancelled');
|
|
62
|
+
if (!result.text?.trim()) throw new Error('Local Pi proactive recovery returned an empty summary');
|
|
63
|
+
// Include framing for Pi's summary message. A fixed percentage cannot be
|
|
64
|
+
// required below the immutable envelope/newest-group floor. Bound both
|
|
65
|
+
// remaining headroom and actual progress instead, using the real summary.
|
|
66
|
+
const summaryBytes = bytes(result.text) + 1024;
|
|
67
|
+
const target = Math.min(budget - headroom, pressure - 4096,
|
|
68
|
+
Math.max(Math.floor(budget * 0.65 * 0.75), floor + summaryBytes));
|
|
69
|
+
let cut, after;
|
|
70
|
+
for (const candidate of starts.slice(-4)) {
|
|
71
|
+
const candidatePressure = historyPressure(history(entries.slice(candidate)), envelope + summaryBytes);
|
|
72
|
+
if (candidatePressure <= target) { cut = candidate; after = candidatePressure; break; }
|
|
73
|
+
}
|
|
74
|
+
if (cut === undefined) throw new Error('Local Pi proactive recovery did not free enough budget; split the task. No fallback.');
|
|
75
|
+
// Re-arm above the measured retained floor, so a productive large write
|
|
76
|
+
// does not cause an immediate repeat summary with nothing new to reclaim.
|
|
77
|
+
nextRecovery = Math.min(budget - Math.floor(headroom / 2), after + 4096);
|
|
78
|
+
writeSync(1, `${JSON.stringify({ type: 'factory_context_recovery', phase: 'complete', pressure: after, target,
|
|
79
|
+
summaryBytes, keptGroups: starts.filter(index => index >= cut).length, recovery: recoveries })}\n`);
|
|
80
|
+
return { entries: [{ type: 'compaction', summary: result.text,
|
|
81
|
+
firstKeptEntryId: entries[cut].sourceEntry.id, usage: result.usage,
|
|
82
|
+
details: { factoryLocalRecovery: 1 } }] };
|
|
83
|
+
} catch (error) { fatal(error); }
|
|
84
|
+
});
|
|
17
85
|
pi.on('session_before_compact', event => {
|
|
18
86
|
try {
|
|
19
87
|
// These are request-local copies. Upstream still selects cut points and
|
|
@@ -25,39 +93,39 @@ export default function factoryLocalContext(pi) {
|
|
|
25
93
|
preparation[key] = fitToolText(preparation[key], budget, bytes, true);
|
|
26
94
|
} catch (error) { fatal(error); }
|
|
27
95
|
});
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
streamSimple
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
payload
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
});
|
|
96
|
+
function streamLocal(model, context, options) {
|
|
97
|
+
if (model.id !== configured.id || model.provider !== 'factory-local') fatal(new Error('Selected local model changed'));
|
|
98
|
+
// Capture before streamSimple/buildBaseOptions clamps against unprojected
|
|
99
|
+
// history. Generation defaults to the binding; summaries supply their cap.
|
|
100
|
+
const requestedOutput = options?.maxTokens ?? output;
|
|
101
|
+
return openAICompletionsApi().streamSimple(model, context, {
|
|
102
|
+
...options,
|
|
103
|
+
onPayload: async (payload, requestModel) => {
|
|
104
|
+
try {
|
|
105
|
+
payload = await options?.onPayload?.(payload, requestModel) ?? payload;
|
|
106
|
+
payload = structuredClone(payload);
|
|
107
|
+
if (payload.model !== configured.id
|
|
108
|
+
|| !payload.messages?.some(m => m.role === 'system')) throw new Error('Unsupported local request structure or output budget');
|
|
109
|
+
restoreOutputBudget(payload, configured, requestedOutput, options?.reasoning);
|
|
110
|
+
if (payload.tools?.length) {
|
|
111
|
+
task ??= structuredClone(payload.messages.find(m => m.role === 'user'));
|
|
112
|
+
if (!task) throw new Error('Missing required task');
|
|
113
|
+
if (!payload.messages.some(m => m.role === 'user' && JSON.stringify(m) === JSON.stringify(task)))
|
|
114
|
+
payload.messages.splice(1, 0, structuredClone(task));
|
|
115
|
+
validatePairs(payload.messages);
|
|
116
|
+
envelope = bytes(payload.tools) + bytes(payload.messages.filter(m => m.role === 'system'))
|
|
117
|
+
+ bytes(task) + 4096;
|
|
118
|
+
}
|
|
119
|
+
if (!payload.messages.some(m => m.role === 'user')) throw new Error('Missing required user message');
|
|
120
|
+
// Restore BEFORE measuring so the final output field is counted.
|
|
121
|
+
// No request is sent unless projection fits with the full configured
|
|
122
|
+
// reserve, even when a summary requests a smaller bounded output.
|
|
123
|
+
return fitToolText(payload, window - output, projectedTokens);
|
|
124
|
+
} catch (error) { fatal(error); }
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
pi.registerProvider('factory-local', { api: 'openai-completions', streamSimple: streamLocal });
|
|
61
129
|
// Launcher requires this handshake; an extension load failure cannot bypass it.
|
|
62
130
|
writeSync(1, `${JSON.stringify({ type: 'factory_context_ready', version: 1 })}\n`);
|
|
63
131
|
}
|
|
@@ -24,6 +24,8 @@ export class PiCompletion {
|
|
|
24
24
|
settled = false;
|
|
25
25
|
lastAssistant = null;
|
|
26
26
|
contextReady = false;
|
|
27
|
+
proactiveRecovery = false;
|
|
28
|
+
proactiveCount = 0;
|
|
27
29
|
|
|
28
30
|
write(chunk) {
|
|
29
31
|
this.pending += chunk;
|
|
@@ -42,6 +44,17 @@ export class PiCompletion {
|
|
|
42
44
|
event(value) {
|
|
43
45
|
if (!value || typeof value.type !== 'string') { this.failed = true; return; }
|
|
44
46
|
if (value.type === 'factory_context_ready' && value.version === 1) this.contextReady = true;
|
|
47
|
+
if (value.type === 'factory_context_recovery') {
|
|
48
|
+
if (value.phase === 'start') {
|
|
49
|
+
if (this.proactiveRecovery || value.recovery !== this.proactiveCount + 1 || value.recovery > 16) this.failed = true;
|
|
50
|
+
this.proactiveCount = value.recovery;
|
|
51
|
+
this.proactiveRecovery = true;
|
|
52
|
+
this.settled = false;
|
|
53
|
+
} else if (value.phase === 'complete') {
|
|
54
|
+
if (!this.proactiveRecovery || value.recovery !== this.proactiveCount) this.failed = true;
|
|
55
|
+
this.proactiveRecovery = false;
|
|
56
|
+
} else this.failed = true;
|
|
57
|
+
}
|
|
45
58
|
if (value.type === 'agent_start' || value.type === 'message_start') {
|
|
46
59
|
this.completed = false; this.settled = false;
|
|
47
60
|
}
|
|
@@ -95,7 +108,7 @@ export class PiCompletion {
|
|
|
95
108
|
|
|
96
109
|
finish() {
|
|
97
110
|
return !this.pending && !this.failed && !this.error && !this.retry && !this.compacting && !this.recovery
|
|
98
|
-
&& this.completed && (!this.modern || this.settled);
|
|
111
|
+
&& !this.proactiveRecovery && this.completed && (!this.modern || this.settled);
|
|
99
112
|
}
|
|
100
113
|
}
|
|
101
114
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "software-defence-factory",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.5",
|
|
4
4
|
"description": "Factory: scoped software delivery and defence investigations with isolated jobs, evidence and review",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"check": "node scripts/check.mjs && node --test tests/*.test.mjs && npm run test:dashboard",
|
|
59
59
|
"build:dashboard": "npm --prefix dashboard ci --ignore-scripts && npm --prefix dashboard run build",
|
|
60
60
|
"test:dashboard": "npm --prefix dashboard test",
|
|
61
|
-
"qualify:pi": "FACTORY_REQUIRE_PI=1 node --test tests/pi-local-adapter.test.mjs tests/pi-trust.test.mjs tests/pi-context.test.mjs tests/pi-context-batch.test.mjs"
|
|
61
|
+
"qualify:pi": "FACTORY_REQUIRE_PI=1 node --test tests/pi-local-adapter.test.mjs tests/pi-trust.test.mjs tests/pi-context.test.mjs tests/pi-context-batch.test.mjs tests/pi-context-history.test.mjs tests/pi-context-productive.test.mjs"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
64
|
"yaml": "2.9.1"
|