@phi-code-admin/phi-code 0.76.16 → 0.77.0
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/CHANGELOG.md +30 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +20 -3
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/api-key-store.d.ts +1 -1
- package/dist/core/api-key-store.d.ts.map +1 -1
- package/dist/core/api-key-store.js +5 -2
- package/dist/core/api-key-store.js.map +1 -1
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +3 -3
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +4 -1
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js +7 -2
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +4 -0
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/keybindings.d.ts +5 -0
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +4 -0
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +6 -2
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +9 -3
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +36 -5
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +15 -4
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +2 -0
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +3 -1
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/output-accumulator.d.ts +1 -0
- package/dist/core/tools/output-accumulator.d.ts.map +1 -1
- package/dist/core/tools/output-accumulator.js +12 -1
- package/dist/core/tools/output-accumulator.js.map +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +3 -1
- package/dist/core/tools/read.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +27 -7
- package/dist/migrations.js.map +1 -1
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-editor.js +16 -1
- package/dist/modes/interactive/components/custom-editor.js.map +1 -1
- package/dist/modes/interactive/components/diff.d.ts.map +1 -1
- package/dist/modes/interactive/components/diff.js +4 -2
- package/dist/modes/interactive/components/diff.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +9 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +29 -4
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/dist/modes/interactive/components/login-dialog.js +10 -4
- package/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +7 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +80 -8
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/utils/sleep.d.ts.map +1 -1
- package/dist/utils/sleep.js +7 -3
- package/dist/utils/sleep.js.map +1 -1
- package/extensions/phi/benchmark.ts +9 -2
- package/extensions/phi/init.ts +4 -1
- package/extensions/phi/keys.ts +13 -0
- package/extensions/phi/memory.ts +4 -1
- package/extensions/phi/orchestrator.ts +115 -25
- package/extensions/phi/providers/opencode-go.ts +22 -12
- package/extensions/phi/setup.ts +8 -1
- package/extensions/phi/web-search.ts +15 -3
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sleep.d.ts","sourceRoot":"","sources":["../../src/utils/sleep.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"sleep.d.ts","sourceRoot":"","sources":["../../src/utils/sleep.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBrE","sourcesContent":["/**\n * Sleep helper that respects abort signal.\n */\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\tif (signal?.aborted) {\n\t\t\treject(new Error(\"Aborted\"));\n\t\t\treturn;\n\t\t}\n\n\t\tconst onAbort = () => {\n\t\t\tclearTimeout(timeout);\n\t\t\treject(new Error(\"Aborted\"));\n\t\t};\n\n\t\tconst timeout = setTimeout(() => {\n\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\tresolve();\n\t\t}, ms);\n\n\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\t});\n}\n"]}
|
package/dist/utils/sleep.js
CHANGED
|
@@ -7,11 +7,15 @@ export function sleep(ms, signal) {
|
|
|
7
7
|
reject(new Error("Aborted"));
|
|
8
8
|
return;
|
|
9
9
|
}
|
|
10
|
-
const
|
|
11
|
-
signal?.addEventListener("abort", () => {
|
|
10
|
+
const onAbort = () => {
|
|
12
11
|
clearTimeout(timeout);
|
|
13
12
|
reject(new Error("Aborted"));
|
|
14
|
-
}
|
|
13
|
+
};
|
|
14
|
+
const timeout = setTimeout(() => {
|
|
15
|
+
signal?.removeEventListener("abort", onAbort);
|
|
16
|
+
resolve();
|
|
17
|
+
}, ms);
|
|
18
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
15
19
|
});
|
|
16
20
|
}
|
|
17
21
|
//# sourceMappingURL=sleep.js.map
|
package/dist/utils/sleep.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sleep.js","sourceRoot":"","sources":["../../src/utils/sleep.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,UAAU,KAAK,CAAC,EAAU,EAAE,MAAoB,EAAiB;IACtE,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC;QACvC,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;YACrB,MAAM,CAAC,IAAI,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;YAC7B,OAAO;QACR,CAAC;QAED,MAAM,OAAO,GAAG,
|
|
1
|
+
{"version":3,"file":"sleep.js","sourceRoot":"","sources":["../../src/utils/sleep.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,UAAU,KAAK,CAAC,EAAU,EAAE,MAAoB,EAAiB;IACtE,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC;QACvC,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;YACrB,MAAM,CAAC,IAAI,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;YAC7B,OAAO;QACR,CAAC;QAED,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC;YACrB,YAAY,CAAC,OAAO,CAAC,CAAC;YACtB,MAAM,CAAC,IAAI,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;QAAA,CAC7B,CAAC;QAEF,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC;YAChC,MAAM,EAAE,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC9C,OAAO,EAAE,CAAC;QAAA,CACV,EAAE,EAAE,CAAC,CAAC;QAEP,MAAM,EAAE,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;IAAA,CAC3D,CAAC,CAAC;AAAA,CACH","sourcesContent":["/**\n * Sleep helper that respects abort signal.\n */\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\tif (signal?.aborted) {\n\t\t\treject(new Error(\"Aborted\"));\n\t\t\treturn;\n\t\t}\n\n\t\tconst onAbort = () => {\n\t\t\tclearTimeout(timeout);\n\t\t\treject(new Error(\"Aborted\"));\n\t\t};\n\n\t\tconst timeout = setTimeout(() => {\n\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\tresolve();\n\t\t}, ms);\n\n\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\t});\n}\n"]}
|
|
@@ -711,9 +711,16 @@ Scoring: S (80+), A (65+), B (50+), C (35+), D (<35)`, "info");
|
|
|
711
711
|
}
|
|
712
712
|
|
|
713
713
|
if (arg) {
|
|
714
|
-
// Benchmark specific model
|
|
715
|
-
|
|
714
|
+
// Benchmark specific model: prefer exact id match, then require an
|
|
715
|
+
// unambiguous substring match to avoid benchmarking the wrong model.
|
|
716
|
+
const exact = available.find(m => m.id.toLowerCase() === arg);
|
|
717
|
+
const matches = available.filter(m => m.id.toLowerCase().includes(arg));
|
|
718
|
+
const model = exact ?? (matches.length === 1 ? matches[0] : undefined);
|
|
716
719
|
if (!model) {
|
|
720
|
+
if (!exact && matches.length > 1) {
|
|
721
|
+
ctx.ui.notify(`Model "${arg}" is ambiguous. Did you mean:\n${matches.map(m => ` - ${m.id} (${m.provider})`).join("\n")}`, "warning");
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
717
724
|
ctx.ui.notify(`Model "${arg}" not found or no API key. Available:\n${available.map(m => ` - ${m.id} (${m.provider})`).join("\n")}`, "warning");
|
|
718
725
|
return;
|
|
719
726
|
}
|
package/extensions/phi/init.ts
CHANGED
|
@@ -502,7 +502,10 @@ _Edit this file to customize Phi Code's behavior for your project._
|
|
|
502
502
|
// not cause the user to lose what they just typed.
|
|
503
503
|
try {
|
|
504
504
|
await persistProviderKey(provider, trimmed);
|
|
505
|
-
process.env
|
|
505
|
+
// Do not mirror the key into process.env: getKey() reads models.json
|
|
506
|
+
// first, so the store is already the source of truth. Setting the env
|
|
507
|
+
// var would leak the plaintext key to every child process (bash tool,
|
|
508
|
+
// pi.exec, npm postinstall, etc.).
|
|
506
509
|
} catch (err) {
|
|
507
510
|
ctx.ui.notify(
|
|
508
511
|
`Failed to write ${modelsJsonPath}: ${err instanceof Error ? err.message : String(err)}`,
|
package/extensions/phi/keys.ts
CHANGED
|
@@ -76,6 +76,19 @@ export default function keysExtension(pi: ExtensionAPI) {
|
|
|
76
76
|
ctx.ui.notify("Usage: `/keys set <provider-id> <api-key>`", "warning");
|
|
77
77
|
return;
|
|
78
78
|
}
|
|
79
|
+
// A bare `set` only updates the key; it cannot supply baseUrl/api/models.
|
|
80
|
+
// For a never-configured id this would persist an unusable provider
|
|
81
|
+
// (apiKey but no endpoint), so require an existing entry with a baseUrl.
|
|
82
|
+
const existing = store.getProvider(id);
|
|
83
|
+
if (!existing?.baseUrl) {
|
|
84
|
+
ctx.ui.notify(
|
|
85
|
+
`\`${id}\` is not a configured provider (no baseUrl on file). ` +
|
|
86
|
+
`Run \`/setup\` to add it with an endpoint and models first; ` +
|
|
87
|
+
`\`/keys set\` only updates the key of an already-configured provider.`,
|
|
88
|
+
"warning",
|
|
89
|
+
);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
79
92
|
watcher.muteForWrite("models_json_changed");
|
|
80
93
|
store.setKey(id, key);
|
|
81
94
|
ctx.ui.notify(
|
package/extensions/phi/memory.ts
CHANGED
|
@@ -436,9 +436,12 @@ export default function memoryExtension(pi: ExtensionAPI) {
|
|
|
436
436
|
return {};
|
|
437
437
|
}
|
|
438
438
|
const truncated = userPrompt.length > 200 ? `${userPrompt.slice(0, 200)}...` : userPrompt;
|
|
439
|
+
// Neutralize angle brackets so user content cannot close the
|
|
440
|
+
// <system-reminder> block early and inject trusted instructions.
|
|
441
|
+
const safe = truncated.replace(/[<>]/g, (c) => (c === "<" ? "<" : ">")).replace(/"/g, '\\"');
|
|
439
442
|
const reminder = `<system-reminder>
|
|
440
443
|
You are about to respond to a new user message:
|
|
441
|
-
"${
|
|
444
|
+
"${safe}"
|
|
442
445
|
|
|
443
446
|
REMINDER (project rule, applies every turn):
|
|
444
447
|
1. Call \`memory_search\` FIRST with keywords from the user's intent. Recent
|
|
@@ -229,6 +229,11 @@ export default function orchestratorExtension(pi: ExtensionAPI) {
|
|
|
229
229
|
const MAX_PHASE_DURATION_MS = 10 * 60 * 1000; // 10 minutes per phase
|
|
230
230
|
const MAX_TOOL_CALLS_PER_PHASE = 60; // Safety limit
|
|
231
231
|
let phaseStartTime: number | null = null;
|
|
232
|
+
// User's model before /plan started, restored once orchestration ends
|
|
233
|
+
let originalModel: any | null = null;
|
|
234
|
+
// Real phase counters for the final summary
|
|
235
|
+
let completedPhases = 0;
|
|
236
|
+
let skippedPhases = 0;
|
|
232
237
|
|
|
233
238
|
/**
|
|
234
239
|
* Parse agent .md file with YAML frontmatter
|
|
@@ -582,27 +587,48 @@ Tag the note with relevant keywords for vector search.
|
|
|
582
587
|
|
|
583
588
|
/**
|
|
584
589
|
* Switch model for the current phase.
|
|
590
|
+
* Returns the model id actually in use plus an optional warning when the
|
|
591
|
+
* requested routing.json model could not be applied (not in registry or no
|
|
592
|
+
* API key), so the caller can surface the degraded routing to the user.
|
|
585
593
|
*/
|
|
586
|
-
async function switchModelForPhase(
|
|
594
|
+
async function switchModelForPhase(
|
|
595
|
+
phase: OrchestratorPhase,
|
|
596
|
+
ctx: any,
|
|
597
|
+
): Promise<{ modelId: string; warning?: string }> {
|
|
587
598
|
const available = ctx.modelRegistry?.getAvailable?.() || [];
|
|
588
599
|
const target = resolveModelRef(available, phase.model) || resolveModelRef(available, phase.fallback);
|
|
600
|
+
const currentId = ctx.model?.id || phase.model;
|
|
589
601
|
|
|
590
|
-
if (target
|
|
602
|
+
if (!target) {
|
|
603
|
+
// Neither the preferred nor the fallback ref resolved to a registered model.
|
|
604
|
+
return {
|
|
605
|
+
modelId: currentId,
|
|
606
|
+
warning: `Phase ${phase.label} model '${phase.model}' unavailable (not in registry); running on '${currentId}'`,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (target.id !== ctx.model?.id || target.provider !== ctx.model?.provider) {
|
|
591
611
|
const switched = await pi.setModel(target);
|
|
592
|
-
if (switched) return target.id;
|
|
612
|
+
if (switched) return { modelId: target.id };
|
|
613
|
+
// setModel returns false when the target has no configured auth.
|
|
614
|
+
return {
|
|
615
|
+
modelId: currentId,
|
|
616
|
+
warning: `Phase ${phase.label} model '${phase.model}' unavailable (no API key); running on '${currentId}'`,
|
|
617
|
+
};
|
|
593
618
|
}
|
|
594
|
-
return
|
|
619
|
+
return { modelId: target.id };
|
|
595
620
|
}
|
|
596
621
|
|
|
597
622
|
/**
|
|
598
623
|
* Activate agent for a phase: set system prompt + restrict tools.
|
|
599
624
|
*/
|
|
600
625
|
function activateAgent(phase: OrchestratorPhase, ctx: any) {
|
|
626
|
+
// Fallback capture: savedTools is normally taken in the /plan handler at
|
|
627
|
+
// orchestration start, but capture it here too so restoration is always defined.
|
|
628
|
+
if (!savedTools) {
|
|
629
|
+
savedTools = pi.getActiveTools();
|
|
630
|
+
}
|
|
601
631
|
if (phase.agent) {
|
|
602
|
-
// Save current tools for restoration
|
|
603
|
-
if (!savedTools) {
|
|
604
|
-
savedTools = pi.getActiveTools();
|
|
605
|
-
}
|
|
606
632
|
// Set agent's system prompt (will be injected via before_agent_start)
|
|
607
633
|
activeAgentPrompt = phase.agent.systemPrompt;
|
|
608
634
|
// Restrict tools to agent's allowed tools
|
|
@@ -612,10 +638,20 @@ Tag the note with relevant keywords for vector search.
|
|
|
612
638
|
const agentTools = [...phase.agent.tools, ...memoryTools.filter(t => !phase.agent.tools.includes(t))];
|
|
613
639
|
activeAgentTools = agentTools;
|
|
614
640
|
pi.setActiveTools(agentTools);
|
|
641
|
+
} else if (savedTools) {
|
|
642
|
+
// Agent with no tool restriction: reset to the full toolset so the
|
|
643
|
+
// previous phase's narrower restriction does not leak into this one.
|
|
644
|
+
activeAgentTools = null;
|
|
645
|
+
pi.setActiveTools(savedTools);
|
|
615
646
|
}
|
|
616
647
|
} else {
|
|
648
|
+
// No agent for this phase: restore the full toolset, otherwise the prior
|
|
649
|
+
// phase's restriction would persist.
|
|
617
650
|
activeAgentPrompt = null;
|
|
618
651
|
activeAgentTools = null;
|
|
652
|
+
if (savedTools) {
|
|
653
|
+
pi.setActiveTools(savedTools);
|
|
654
|
+
}
|
|
619
655
|
}
|
|
620
656
|
}
|
|
621
657
|
|
|
@@ -639,27 +675,54 @@ Tag the note with relevant keywords for vector search.
|
|
|
639
675
|
(globalThis as any).__phiOrchestrationActive = active;
|
|
640
676
|
}
|
|
641
677
|
|
|
678
|
+
/**
|
|
679
|
+
* Tear down orchestration state without chaining to the next phase.
|
|
680
|
+
* Used by the user-abort and auth-error paths so they restore the user's
|
|
681
|
+
* model and tools exactly like the normal completion branch.
|
|
682
|
+
*/
|
|
683
|
+
function stopOrchestration() {
|
|
684
|
+
setOrchestrationActive(false);
|
|
685
|
+
phasePending = false;
|
|
686
|
+
deactivateAgent();
|
|
687
|
+
if (phaseTimeoutId) { clearTimeout(phaseTimeoutId); phaseTimeoutId = null; }
|
|
688
|
+
if (originalModel) {
|
|
689
|
+
const toRestore = originalModel;
|
|
690
|
+
originalModel = null;
|
|
691
|
+
Promise.resolve(pi.setModel(toRestore)).catch(() => { /* best effort */ });
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
642
695
|
function sendNextPhase(ctx: any) {
|
|
643
696
|
if (phaseQueue.length === 0) {
|
|
644
697
|
// All phases done — clean up and notify
|
|
645
698
|
setOrchestrationActive(false);
|
|
646
699
|
phasePending = false;
|
|
647
700
|
deactivateAgent();
|
|
701
|
+
// Restore the user's model so /plan does not overwrite their /model choice.
|
|
702
|
+
if (originalModel) {
|
|
703
|
+
const toRestore = originalModel;
|
|
704
|
+
originalModel = null;
|
|
705
|
+
Promise.resolve(pi.setModel(toRestore)).catch(() => { /* best effort */ });
|
|
706
|
+
}
|
|
648
707
|
if (phaseTimeoutId) { clearTimeout(phaseTimeoutId); phaseTimeoutId = null; }
|
|
649
|
-
// Generate global final summary
|
|
650
|
-
const totalPhases = 5; // always 5
|
|
708
|
+
// Generate global final summary using the real phase counters
|
|
651
709
|
const elapsed = phaseStartTime ? Math.round((Date.now() - phaseStartTime) / 1000) : 0;
|
|
652
710
|
const minutes = Math.floor(elapsed / 60);
|
|
653
711
|
const seconds = elapsed % 60;
|
|
712
|
+
const skippedNote = skippedPhases ? ` (${skippedPhases} skipped on timeout)` : "";
|
|
654
713
|
ctx.ui.notify(`\n📊 **Orchestration Summary**\n` +
|
|
655
|
-
` Phases: ${
|
|
714
|
+
` Phases: ${completedPhases}/5 completed${skippedNote}\n` +
|
|
656
715
|
` Duration: ${minutes}m ${seconds}s\n` +
|
|
657
716
|
` Check \`.phi/plans/\` for all reports`, "info");
|
|
717
|
+
const allDone = completedPhases >= 5 && skippedPhases === 0;
|
|
718
|
+
const doneMsg = allDone
|
|
719
|
+
? `\n✅ **All 5 phases complete!**`
|
|
720
|
+
: `\n⚠️ **Orchestration finished:** ${completedPhases}/5 phases completed${skippedNote}.`;
|
|
658
721
|
try {
|
|
659
|
-
ctx.ui.notify(
|
|
722
|
+
ctx.ui.notify(doneMsg, "info");
|
|
660
723
|
} catch {
|
|
661
724
|
// Fallback: inject completion message into conversation stream
|
|
662
|
-
try { pi.sendUserMessage(
|
|
725
|
+
try { pi.sendUserMessage(`Orchestration finished: ${completedPhases}/5 phases completed${skippedNote}.`); } catch { /* best effort */ }
|
|
663
726
|
}
|
|
664
727
|
return;
|
|
665
728
|
}
|
|
@@ -667,18 +730,25 @@ Tag the note with relevant keywords for vector search.
|
|
|
667
730
|
const phase = phaseQueue.shift()!;
|
|
668
731
|
phasePending = true;
|
|
669
732
|
|
|
670
|
-
switchModelForPhase(phase, ctx).then((modelId) => {
|
|
733
|
+
switchModelForPhase(phase, ctx).then(({ modelId, warning }) => {
|
|
671
734
|
activateAgent(phase, ctx);
|
|
672
735
|
const agentName = phase.agent?.name || phase.key;
|
|
673
736
|
ctx.ui.notify(`\n${phase.label} → \`${modelId}\` (agent: ${agentName})`, "info");
|
|
674
|
-
|
|
675
|
-
|
|
737
|
+
if (warning) ctx.ui.notify(`\n⚠️ ${warning}`, "warning");
|
|
738
|
+
// Small delay to let the model switch settle, then send instruction.
|
|
739
|
+
// deliverAs:"followUp" queues the message instead of throwing when the
|
|
740
|
+
// agent is still streaming (e.g. on the timeout path).
|
|
741
|
+
setTimeout(() => pi.sendUserMessage(phase.instruction, { deliverAs: "followUp" }), 500);
|
|
676
742
|
// Set phase timeout — abort if phase takes too long
|
|
677
743
|
if (phaseTimeoutId) clearTimeout(phaseTimeoutId);
|
|
678
744
|
phaseTimeoutId = setTimeout(() => {
|
|
679
745
|
if (orchestrationActive && phasePending) {
|
|
680
746
|
ctx.ui.notify(`\n⏰ **Phase timed out** (${MAX_PHASE_DURATION_MS / 60000} min limit). Skipping to next phase.`, "warning");
|
|
681
747
|
phasePending = false;
|
|
748
|
+
skippedPhases++;
|
|
749
|
+
// Abort the stuck phase first so the next instruction does not
|
|
750
|
+
// race a still-streaming phase.
|
|
751
|
+
try { ctx.abort(); } catch { /* best effort */ }
|
|
682
752
|
sendNextPhase(ctx);
|
|
683
753
|
}
|
|
684
754
|
}, MAX_PHASE_DURATION_MS);
|
|
@@ -705,6 +775,19 @@ Tag the note with relevant keywords for vector search.
|
|
|
705
775
|
|
|
706
776
|
pi.on("agent_end", async (event, ctx) => {
|
|
707
777
|
if (!orchestrationActive) return;
|
|
778
|
+
|
|
779
|
+
// User abort (Ctrl+C / ESC): the aborted run still emits agent_end, but it
|
|
780
|
+
// must NOT be treated as phase completion. Detect the aborted assistant
|
|
781
|
+
// message and stop the whole workflow instead of chaining the next phase.
|
|
782
|
+
const userAborted = (event.messages || []).some(
|
|
783
|
+
(m: any) => m.role === 'assistant' && m.stopReason === 'aborted',
|
|
784
|
+
);
|
|
785
|
+
if (userAborted) {
|
|
786
|
+
ctx.ui.notify(`\n🛑 **Orchestration cancelled** by user. Remaining phases skipped.`, "warning");
|
|
787
|
+
stopOrchestration();
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
|
|
708
791
|
// Allow completion even if phasePending was cleared by a duplicate event
|
|
709
792
|
if (!phasePending) {
|
|
710
793
|
// If queue is empty and orchestration is active, force completion
|
|
@@ -740,9 +823,10 @@ Tag the note with relevant keywords for vector search.
|
|
|
740
823
|
const match = content.match(/wrote \d+ bytes to (.+)/);
|
|
741
824
|
if (match) filesWritten.push(match[1]);
|
|
742
825
|
}
|
|
743
|
-
// Track edits
|
|
744
|
-
|
|
745
|
-
|
|
826
|
+
// Track edits — the edit tool returns "Successfully replaced N block(s) in <path>."
|
|
827
|
+
// Anchor the path capture so it does not over-capture unrelated text.
|
|
828
|
+
if (name === 'edit' && !content.includes('ERR') && !(msg as any).isError) {
|
|
829
|
+
const match = content.match(/replaced \d+ block\(s\) in (.+?)\.?$/m);
|
|
746
830
|
if (match) filesEdited.push(match[1]);
|
|
747
831
|
}
|
|
748
832
|
// Track errors — but filter out edit retries (old_text mismatch = normal retry, not error)
|
|
@@ -770,10 +854,7 @@ Tag the note with relevant keywords for vector search.
|
|
|
770
854
|
// Only a genuine auth failure (401) is fatal — abort the whole workflow.
|
|
771
855
|
if (hasAuthError) {
|
|
772
856
|
ctx.ui.notify(`\n❌ **Orchestrator aborted:** API authentication error (401)\nCheck your API key and model configuration.`, "error");
|
|
773
|
-
|
|
774
|
-
phasePending = false;
|
|
775
|
-
deactivateAgent();
|
|
776
|
-
if (phaseTimeoutId) { clearTimeout(phaseTimeoutId); phaseTimeoutId = null; }
|
|
857
|
+
stopOrchestration();
|
|
777
858
|
return;
|
|
778
859
|
}
|
|
779
860
|
|
|
@@ -816,6 +897,7 @@ Tag the note with relevant keywords for vector search.
|
|
|
816
897
|
}
|
|
817
898
|
|
|
818
899
|
// Phase complete — chain to next
|
|
900
|
+
completedPhases++;
|
|
819
901
|
phasePending = false;
|
|
820
902
|
sendNextPhase(ctx);
|
|
821
903
|
});
|
|
@@ -851,6 +933,13 @@ Tag the note with relevant keywords for vector search.
|
|
|
851
933
|
phaseQueue = phases.slice(1); // Queue phases 2-5
|
|
852
934
|
setOrchestrationActive(true);
|
|
853
935
|
phasePending = true;
|
|
936
|
+
// Capture the user's model + full toolset so they can be restored when
|
|
937
|
+
// orchestration ends (otherwise /plan permanently overwrites /model and
|
|
938
|
+
// per-phase tool restrictions can leak).
|
|
939
|
+
originalModel = ctx.model || null;
|
|
940
|
+
savedTools = pi.getActiveTools();
|
|
941
|
+
completedPhases = 0;
|
|
942
|
+
skippedPhases = 0;
|
|
854
943
|
const firstPhase = phases[0];
|
|
855
944
|
|
|
856
945
|
ctx.ui.notify(`📋 **Orchestrator started** — 5 phases with model routing + agent roles\n`, "info");
|
|
@@ -866,11 +955,12 @@ Tag the note with relevant keywords for vector search.
|
|
|
866
955
|
// Record orchestration start time for final summary
|
|
867
956
|
phaseStartTime = Date.now();
|
|
868
957
|
// Switch model and activate agent for first phase
|
|
869
|
-
const modelId = await switchModelForPhase(firstPhase, ctx);
|
|
958
|
+
const { modelId, warning } = await switchModelForPhase(firstPhase, ctx);
|
|
870
959
|
activateAgent(firstPhase, ctx);
|
|
871
960
|
const agentName = firstPhase.agent?.name || firstPhase.key;
|
|
872
961
|
ctx.ui.notify(`${firstPhase.label} → \`${modelId}\` (agent: ${agentName})`, "info");
|
|
873
|
-
|
|
962
|
+
if (warning) ctx.ui.notify(`\n⚠️ ${warning}`, "warning");
|
|
963
|
+
setTimeout(() => pi.sendUserMessage(firstPhase.instruction, { deliverAs: "followUp" }), 200);
|
|
874
964
|
},
|
|
875
965
|
});
|
|
876
966
|
|
|
@@ -77,7 +77,9 @@ interface CacheEntry {
|
|
|
77
77
|
|
|
78
78
|
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
79
79
|
let cache: CacheEntry | null = null;
|
|
80
|
-
|
|
80
|
+
// Inflight fetches keyed by apiKey so a keyless probe never leaks its
|
|
81
|
+
// (possibly 401/reduced) result to a caller that passed a real key.
|
|
82
|
+
const inflightFetch = new Map<string, Promise<OpenCodeGoModel[]>>();
|
|
81
83
|
|
|
82
84
|
function isCacheValid(now: number): boolean {
|
|
83
85
|
return cache !== null && now - cache.fetchedAt < CACHE_TTL_MS;
|
|
@@ -125,19 +127,25 @@ export async function getOpenCodeGoModels(options?: {
|
|
|
125
127
|
return { models: cache!.models, source: "cache" };
|
|
126
128
|
}
|
|
127
129
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
const inflightKey = options?.apiKey ?? "";
|
|
131
|
+
|
|
132
|
+
if (!force) {
|
|
133
|
+
const existing = inflightFetch.get(inflightKey);
|
|
134
|
+
if (existing) {
|
|
135
|
+
try {
|
|
136
|
+
const models = await existing;
|
|
137
|
+
return { models, source: "live" };
|
|
138
|
+
} catch {
|
|
139
|
+
// fall through to fresh attempt
|
|
140
|
+
}
|
|
134
141
|
}
|
|
135
142
|
}
|
|
136
143
|
|
|
137
|
-
|
|
144
|
+
const promise = fetchModelsRaw(options?.apiKey, timeoutMs);
|
|
145
|
+
inflightFetch.set(inflightKey, promise);
|
|
138
146
|
try {
|
|
139
|
-
const models = await
|
|
140
|
-
cache = { models, fetchedAt: now };
|
|
147
|
+
const models = await promise;
|
|
148
|
+
cache = { models, fetchedAt: Date.now() };
|
|
141
149
|
return { models, source: "live" };
|
|
142
150
|
} catch {
|
|
143
151
|
if (cache) {
|
|
@@ -145,7 +153,9 @@ export async function getOpenCodeGoModels(options?: {
|
|
|
145
153
|
}
|
|
146
154
|
return { models: [...OPENCODE_GO_FALLBACK_MODELS], source: "fallback" };
|
|
147
155
|
} finally {
|
|
148
|
-
|
|
156
|
+
// Only clear the slot we created; a concurrent fetch for the same key
|
|
157
|
+
// may have replaced it (e.g. force=true overwriting a pending entry).
|
|
158
|
+
if (inflightFetch.get(inflightKey) === promise) inflightFetch.delete(inflightKey);
|
|
149
159
|
}
|
|
150
160
|
}
|
|
151
161
|
|
|
@@ -240,5 +250,5 @@ export function buildOpenCodeGoAnthropicProviderConfig(apiKey: string, models: O
|
|
|
240
250
|
*/
|
|
241
251
|
export function _resetOpenCodeGoCache(): void {
|
|
242
252
|
cache = null;
|
|
243
|
-
inflightFetch
|
|
253
|
+
inflightFetch.clear();
|
|
244
254
|
}
|
package/extensions/phi/setup.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* - Storage chmod 0600 garantit la sécurité au repos
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import { ApiKeyStore, type ExtensionAPI, type ExtensionUIContext, getApiKeyStore } from "phi-code";
|
|
22
|
+
import { ApiKeyStore, type ExtensionAPI, type ExtensionUIContext, getApiKeyStore, getConfigWatcher } from "phi-code";
|
|
23
23
|
import { writeFile, mkdir } from "node:fs/promises";
|
|
24
24
|
import { join } from "node:path";
|
|
25
25
|
import { homedir } from "node:os";
|
|
@@ -443,7 +443,13 @@ async function configureGenericCloud(
|
|
|
443
443
|
if (!proceed) return undefined;
|
|
444
444
|
}
|
|
445
445
|
|
|
446
|
+
// Mute the config watcher so these programmatic writes are not echoed back as
|
|
447
|
+
// models_json_changed events. Mute per-write because the ping/fetch below can
|
|
448
|
+
// exceed the ignore window between the two setKey calls (cf. models.ts).
|
|
449
|
+
const watcher = getConfigWatcher();
|
|
450
|
+
|
|
446
451
|
// Persist the key immediately so a downstream fetch failure cannot lose user input.
|
|
452
|
+
watcher.muteForWrite("models_json_changed");
|
|
447
453
|
store.setKey(provider.id, trimmed, {
|
|
448
454
|
baseUrl: provider.baseUrl,
|
|
449
455
|
api: provider.api,
|
|
@@ -479,6 +485,7 @@ async function configureGenericCloud(
|
|
|
479
485
|
: provider.staticModels.map((id) => ({ id, name: id, reasoning: true }))
|
|
480
486
|
).map(toPersistedModel);
|
|
481
487
|
|
|
488
|
+
watcher.muteForWrite("models_json_changed");
|
|
482
489
|
store.setKey(provider.id, trimmed, {
|
|
483
490
|
baseUrl: provider.baseUrl,
|
|
484
491
|
api: provider.api,
|
|
@@ -50,8 +50,20 @@ function decodeEntities(text: string): string {
|
|
|
50
50
|
return text
|
|
51
51
|
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")
|
|
52
52
|
.replace(/"/g, '"').replace(/'/g, "'").replace(/'/g, "'")
|
|
53
|
-
.replace(/&#(\d+);/g, (
|
|
54
|
-
|
|
53
|
+
.replace(/&#(\d+);/g, (m, n) => {
|
|
54
|
+
try {
|
|
55
|
+
return String.fromCodePoint(parseInt(n, 10));
|
|
56
|
+
} catch {
|
|
57
|
+
return m;
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
.replace(/&#x([0-9a-fA-F]+);/g, (m, n) => {
|
|
61
|
+
try {
|
|
62
|
+
return String.fromCodePoint(parseInt(n, 16));
|
|
63
|
+
} catch {
|
|
64
|
+
return m;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
55
67
|
}
|
|
56
68
|
|
|
57
69
|
function stripTags(html: string): string {
|
|
@@ -434,7 +446,7 @@ export default function webSearchExtension(pi: ExtensionAPI) {
|
|
|
434
446
|
const hasReadability = await tryLoadReadability();
|
|
435
447
|
if (hasReadability && _JSDOM && _Readability) {
|
|
436
448
|
try {
|
|
437
|
-
const dom = new _JSDOM(text, { url });
|
|
449
|
+
const dom = new _JSDOM(text, { url: currentUrl });
|
|
438
450
|
const reader = new _Readability(dom.window.document);
|
|
439
451
|
const article = reader.parse();
|
|
440
452
|
if (article?.textContent) {
|