@openeryc/pi-coding-agent 0.75.57 → 0.75.59
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 +28 -0
- package/dist/core/agent-session.d.ts +11 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +32 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/session-manager.d.ts +4 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +19 -2
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts +2 -0
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +8 -1
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/modes/interactive/components/login-dialog.d.ts +7 -1
- package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/dist/modes/interactive/components/login-dialog.js +14 -4
- package/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/dist/modes/interactive/goal-evaluator.d.ts +23 -0
- package/dist/modes/interactive/goal-evaluator.d.ts.map +1 -0
- package/dist/modes/interactive/goal-evaluator.js +68 -0
- package/dist/modes/interactive/goal-evaluator.js.map +1 -0
- package/dist/modes/interactive/goal-runner-utils.d.ts +11 -0
- package/dist/modes/interactive/goal-runner-utils.d.ts.map +1 -0
- package/dist/modes/interactive/goal-runner-utils.js +39 -0
- package/dist/modes/interactive/goal-runner-utils.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts +6 -2
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +147 -47
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +12 -12
- package/package.json +4 -4
|
@@ -62,6 +62,7 @@ import { ToolExecutionComponent } from "./components/tool-execution.js";
|
|
|
62
62
|
import { TreeSelectorComponent } from "./components/tree-selector.js";
|
|
63
63
|
import { UserMessageComponent } from "./components/user-message.js";
|
|
64
64
|
import { UserMessageSelectorComponent } from "./components/user-message-selector.js";
|
|
65
|
+
import { evaluateGoalCondition } from "./goal-evaluator.js";
|
|
65
66
|
import { getAvailableThemes, getAvailableThemesWithPaths, getEditorTheme, getMarkdownTheme, getThemeByName, initTheme, onThemeChange, setRegisteredThemes, setTheme, setThemeInstance, stopThemeWatcher, Theme, theme, } from "./theme/theme.js";
|
|
66
67
|
function isExpandable(obj) {
|
|
67
68
|
return typeof obj === "object" && obj !== null && "setExpanded" in obj && typeof obj.setExpanded === "function";
|
|
@@ -181,9 +182,12 @@ export class InteractiveMode {
|
|
|
181
182
|
goalRunnerAbortController = undefined;
|
|
182
183
|
goalRunnerRetryAttempt = 0;
|
|
183
184
|
goalRunnerStatusDisplayed = false;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
185
|
+
goalRunnerTurnCount = 0;
|
|
186
|
+
goalRunnerStartTime = 0;
|
|
187
|
+
goalRunnerTokenSpend = 0;
|
|
188
|
+
goalRunnerLastEvalReason = "";
|
|
189
|
+
GOAL_RUNNER_TURN_DELAY_MS = 1200;
|
|
190
|
+
GOAL_RUNNER_RETRY_DELAY_MS = 5000;
|
|
187
191
|
// Extension UI state
|
|
188
192
|
extensionSelector = undefined;
|
|
189
193
|
extensionInput = undefined;
|
|
@@ -2072,8 +2076,10 @@ export class InteractiveMode {
|
|
|
2072
2076
|
// Handle commands (with special case for /goal which continues to submit)
|
|
2073
2077
|
if (text.startsWith("/goal")) {
|
|
2074
2078
|
const goal = text.replace(/^\/goal\s*/, "").trim();
|
|
2075
|
-
this.handleGoalCommand(text);
|
|
2079
|
+
const shouldSubmit = await this.handleGoalCommand(text);
|
|
2076
2080
|
this.editor.setText("");
|
|
2081
|
+
if (!shouldSubmit)
|
|
2082
|
+
return;
|
|
2077
2083
|
if (goal)
|
|
2078
2084
|
text = goal;
|
|
2079
2085
|
}
|
|
@@ -4031,7 +4037,9 @@ export class InteractiveMode {
|
|
|
4031
4037
|
this.ui.requestRender();
|
|
4032
4038
|
};
|
|
4033
4039
|
try {
|
|
4034
|
-
const rawName = (await dialog.showPrompt(isEdit ? "Provider name/id:" : "Provider name/id (e.g. ollama, agnes):",
|
|
4040
|
+
const rawName = (await dialog.showPrompt(isEdit ? "Provider name/id:" : "Provider name/id (e.g. ollama, agnes):", isEdit
|
|
4041
|
+
? { value: existing?.providerId ?? "" }
|
|
4042
|
+
: { placeholder: "ollama", value: existing?.providerId ?? "ollama" })).trim();
|
|
4035
4043
|
const providerId = slugifyProviderId(rawName);
|
|
4036
4044
|
if (!providerId) {
|
|
4037
4045
|
throw new Error("Provider name cannot be empty.");
|
|
@@ -4039,7 +4047,9 @@ export class InteractiveMode {
|
|
|
4039
4047
|
if (BUILT_IN_MODEL_PROVIDERS.has(providerId) && options.target === "openai_compatible") {
|
|
4040
4048
|
throw new Error(`"${providerId}" is a built-in provider. Use a different name, or add it via Manage → models.json override.`);
|
|
4041
4049
|
}
|
|
4042
|
-
const baseUrl = (await dialog.showPrompt("Enter base URL:", existing?.baseUrl
|
|
4050
|
+
const baseUrl = (await dialog.showPrompt("Enter base URL:", isEdit || existing?.baseUrl
|
|
4051
|
+
? { value: existing?.baseUrl || "http://localhost:11434/v1" }
|
|
4052
|
+
: { placeholder: "http://localhost:11434/v1", value: "http://localhost:11434/v1" })).trim();
|
|
4043
4053
|
if (!baseUrl) {
|
|
4044
4054
|
throw new Error("Base URL cannot be empty.");
|
|
4045
4055
|
}
|
|
@@ -4050,7 +4060,8 @@ export class InteractiveMode {
|
|
|
4050
4060
|
: isEdit
|
|
4051
4061
|
? "API key (blank keeps current; type not-needed for local):"
|
|
4052
4062
|
: "Enter API key (or leave blank for local endpoints):";
|
|
4053
|
-
const
|
|
4063
|
+
const apiKeyCurrent = existing?.apiKey === "not-needed" ? "" : (existing?.apiKey ?? "");
|
|
4064
|
+
const apiKey = (await dialog.showPrompt(apiKeyPrompt, isEdit || apiKeyCurrent ? { value: apiKeyCurrent } : { placeholder: "sk-... or leave blank" })).trim();
|
|
4054
4065
|
// Blank on edit keeps the existing apiKey (models.json and openai_compatible).
|
|
4055
4066
|
const effectiveApiKey = apiKey ? apiKey : isEdit ? undefined : "not-needed";
|
|
4056
4067
|
dialog.showInfo([theme.fg("muted", "Discovering models from /models ...")]);
|
|
@@ -4119,7 +4130,7 @@ export class InteractiveMode {
|
|
|
4119
4130
|
]);
|
|
4120
4131
|
this.ui.requestRender();
|
|
4121
4132
|
const defaultIds = existing?.models.map((m) => m.id).join(", ") || "llama3.1";
|
|
4122
|
-
const modelNames = (await dialog.showPrompt("Enter model name(s) (comma-separated):", defaultIds)).trim();
|
|
4133
|
+
const modelNames = (await dialog.showPrompt("Enter model name(s) (comma-separated):", { value: defaultIds })).trim();
|
|
4123
4134
|
if (!modelNames) {
|
|
4124
4135
|
throw new Error("At least one model name is required.");
|
|
4125
4136
|
}
|
|
@@ -4583,60 +4594,115 @@ export class InteractiveMode {
|
|
|
4583
4594
|
this.chatContainer.addChild(new Text(theme.fg("dim", `Session name set: ${name}`), 1, 0));
|
|
4584
4595
|
this.ui.requestRender();
|
|
4585
4596
|
}
|
|
4586
|
-
handleGoalCommand(text) {
|
|
4597
|
+
async handleGoalCommand(text) {
|
|
4587
4598
|
const goal = text.replace(/^\/goal\s*/, "").trim();
|
|
4588
4599
|
if (!goal) {
|
|
4600
|
+
// Show goal status (like Claude Code's /goal with no args)
|
|
4589
4601
|
const currentGoal = this.session.sessionGoal;
|
|
4590
|
-
|
|
4591
|
-
|
|
4602
|
+
if (!currentGoal) {
|
|
4603
|
+
this.showWarning("Usage: /goal <condition>\n /goal clear|stop|off|reset|cancel - clear goal");
|
|
4604
|
+
this.ui.requestRender();
|
|
4605
|
+
return false;
|
|
4606
|
+
}
|
|
4607
|
+
const elapsed = this.goalRunnerStartTime > 0 ? this.formatDuration(Date.now() - this.goalRunnerStartTime) : "-";
|
|
4608
|
+
const turns = this.goalRunnerTurnCount > 0 ? String(this.goalRunnerTurnCount) : "-";
|
|
4609
|
+
const tokens = this.goalRunnerTokenSpend > 0 ? `${this.goalRunnerTokenSpend.toLocaleString()} tokens` : "-";
|
|
4610
|
+
let info = `${theme.bold(theme.fg("accent", "Session Goal"))}\n\n`;
|
|
4611
|
+
info += `${theme.fg("dim", "Condition:")} ${currentGoal}\n`;
|
|
4612
|
+
info += `${theme.fg("dim", "Running:")} ${elapsed}\n`;
|
|
4613
|
+
info += `${theme.fg("dim", "Turns:")} ${turns}\n`;
|
|
4614
|
+
info += `${theme.fg("dim", "Tokens:")} ${tokens}\n`;
|
|
4615
|
+
if (this.goalRunnerLastEvalReason) {
|
|
4616
|
+
info += `\n${theme.fg("dim", "Last evaluation:")} ${this.goalRunnerLastEvalReason}\n`;
|
|
4617
|
+
}
|
|
4618
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
4619
|
+
this.chatContainer.addChild(new Markdown(info, 1, 0, this.getMarkdownThemeWithSettings()));
|
|
4620
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
4621
|
+
this.chatContainer.addChild(new Text(theme.fg("dim", ` /goal clear - clear goal`), 1, 0));
|
|
4622
|
+
this.ui.requestRender();
|
|
4623
|
+
return false;
|
|
4624
|
+
}
|
|
4625
|
+
// Handle clear / stop / off / reset / cancel aliases (like Claude Code)
|
|
4626
|
+
const lower = goal.toLowerCase();
|
|
4627
|
+
const clearAliases = new Set(["clear", "stop", "off", "reset", "none", "cancel"]);
|
|
4628
|
+
if (clearAliases.has(lower)) {
|
|
4629
|
+
const prevGoal = this.session.sessionGoal;
|
|
4630
|
+
if (prevGoal) {
|
|
4631
|
+
this.goalRunnerEnabled = false;
|
|
4632
|
+
this.goalRunnerAbortController?.abort();
|
|
4633
|
+
this.goalRunnerTurnCount = 0;
|
|
4634
|
+
this.goalRunnerStartTime = 0;
|
|
4635
|
+
this.goalRunnerTokenSpend = 0;
|
|
4636
|
+
this.goalRunnerLastEvalReason = "";
|
|
4637
|
+
// Clear the goal from session and rebuild system prompt
|
|
4638
|
+
this.session.clearSessionGoal();
|
|
4592
4639
|
this.chatContainer.addChild(new Spacer(1));
|
|
4593
|
-
this.chatContainer.addChild(new Text(theme.fg("dim", `
|
|
4594
|
-
this.chatContainer.addChild(new Text(theme.fg("dim", ` /goal on - enable goal runner`), 1, 0));
|
|
4595
|
-
this.chatContainer.addChild(new Text(theme.fg("dim", ` /goal off - pause goal runner`), 1, 0));
|
|
4640
|
+
this.chatContainer.addChild(new Text(theme.fg("dim", `Goal cleared: "${prevGoal}"`), 1, 0));
|
|
4596
4641
|
}
|
|
4597
4642
|
else {
|
|
4598
|
-
this.showWarning("
|
|
4643
|
+
this.showWarning("No active goal to clear.");
|
|
4599
4644
|
}
|
|
4600
4645
|
this.ui.requestRender();
|
|
4601
|
-
return;
|
|
4646
|
+
return false;
|
|
4602
4647
|
}
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
this.goalRunnerEnabled = lower === "on";
|
|
4648
|
+
if (lower === "on") {
|
|
4649
|
+
this.goalRunnerEnabled = true;
|
|
4606
4650
|
const currentGoal = this.session.sessionGoal;
|
|
4607
4651
|
if (currentGoal) {
|
|
4608
|
-
const label = lower === "on" ? theme.fg("success", "enabled") : theme.fg("warning", "paused");
|
|
4609
4652
|
this.chatContainer.addChild(new Spacer(1));
|
|
4610
|
-
this.chatContainer.addChild(new Text(theme.fg("
|
|
4653
|
+
this.chatContainer.addChild(new Text(theme.fg("success", `Goal runner enabled for: "${currentGoal}"`), 1, 0));
|
|
4611
4654
|
}
|
|
4612
4655
|
else {
|
|
4613
|
-
this.showWarning("No goal is set. Use /goal <
|
|
4656
|
+
this.showWarning("No goal is set. Use /goal <condition> to set one first.");
|
|
4614
4657
|
}
|
|
4615
4658
|
this.ui.requestRender();
|
|
4616
|
-
return;
|
|
4659
|
+
return false;
|
|
4617
4660
|
}
|
|
4661
|
+
// Set new goal — like Claude Code, the goal text is submitted as the next user message
|
|
4618
4662
|
this.session.setSessionGoal(goal);
|
|
4619
4663
|
this.goalRunnerEnabled = true;
|
|
4664
|
+
this.goalRunnerTurnCount = 0;
|
|
4665
|
+
this.goalRunnerStartTime = Date.now();
|
|
4666
|
+
this.goalRunnerTokenSpend = 0;
|
|
4667
|
+
this.goalRunnerLastEvalReason = "";
|
|
4620
4668
|
this.chatContainer.addChild(new Spacer(1));
|
|
4621
|
-
this.chatContainer.addChild(new Text(theme.fg("dim", `Session goal set: ${goal}
|
|
4669
|
+
this.chatContainer.addChild(new Text(theme.fg("dim", `Session goal set: "${goal}" — working toward it...`), 1, 0));
|
|
4622
4670
|
this.ui.requestRender();
|
|
4671
|
+
return true;
|
|
4672
|
+
}
|
|
4673
|
+
formatDuration(ms) {
|
|
4674
|
+
const seconds = Math.floor(ms / 1000);
|
|
4675
|
+
if (seconds < 60)
|
|
4676
|
+
return `${seconds}s`;
|
|
4677
|
+
const minutes = Math.floor(seconds / 60);
|
|
4678
|
+
const secs = seconds % 60;
|
|
4679
|
+
if (minutes < 60)
|
|
4680
|
+
return `${minutes}m ${secs}s`;
|
|
4681
|
+
const hours = Math.floor(minutes / 60);
|
|
4682
|
+
const mins = minutes % 60;
|
|
4683
|
+
return `${hours}h ${mins}m ${secs}s`;
|
|
4623
4684
|
}
|
|
4624
4685
|
/**
|
|
4625
|
-
* Goal runner continuation loop.
|
|
4626
|
-
*
|
|
4627
|
-
*
|
|
4686
|
+
* Goal runner continuation loop — Claude Code-style evaluator loop.
|
|
4687
|
+
*
|
|
4688
|
+
* After each turn:
|
|
4689
|
+
* 1. Runs the evaluator to check if the condition is met
|
|
4690
|
+
* 2. If met: shows achievement and stops
|
|
4691
|
+
* 3. If not met: continues with next turn
|
|
4692
|
+
*
|
|
4693
|
+
* Handles errors with exponential backoff.
|
|
4694
|
+
* User can interrupt with the interrupt key.
|
|
4628
4695
|
* Survives crashes: on next startup, the persisted session goal triggers auto-resume.
|
|
4629
4696
|
*/
|
|
4630
4697
|
async goalRunnerContinuationLoop(goal) {
|
|
4631
|
-
// Abort any previous goal runner controller
|
|
4632
4698
|
this.goalRunnerAbortController?.abort();
|
|
4633
4699
|
this.goalRunnerAbortController = new AbortController();
|
|
4634
4700
|
const signal = this.goalRunnerAbortController.signal;
|
|
4701
|
+
let nextDirection;
|
|
4635
4702
|
try {
|
|
4636
4703
|
while (this.goalRunnerEnabled && this.session.sessionGoal) {
|
|
4637
4704
|
if (signal.aborted)
|
|
4638
4705
|
break;
|
|
4639
|
-
// Small delay between turns so the user can see what happened
|
|
4640
4706
|
try {
|
|
4641
4707
|
await sleep(this.GOAL_RUNNER_TURN_DELAY_MS, signal);
|
|
4642
4708
|
}
|
|
@@ -4645,35 +4711,69 @@ export class InteractiveMode {
|
|
|
4645
4711
|
}
|
|
4646
4712
|
if (signal.aborted)
|
|
4647
4713
|
break;
|
|
4648
|
-
|
|
4649
|
-
if (this.session.isStreaming || this.session.isCompacting) {
|
|
4714
|
+
if (this.session.isStreaming || this.session.isCompacting)
|
|
4650
4715
|
continue;
|
|
4651
|
-
}
|
|
4652
|
-
// Show status on first iteration
|
|
4653
4716
|
if (!this.goalRunnerStatusDisplayed) {
|
|
4654
4717
|
this.goalRunnerStatusDisplayed = true;
|
|
4655
|
-
this.showStatus(`
|
|
4718
|
+
this.showStatus(`Goal: "${goal}" (${keyText("app.interrupt")} to pause, /goal clear to clear)`);
|
|
4656
4719
|
}
|
|
4657
4720
|
try {
|
|
4658
|
-
|
|
4659
|
-
|
|
4721
|
+
// Use evaluator's nextDirection, or fall back to generic prompt
|
|
4722
|
+
const promptText = nextDirection
|
|
4723
|
+
? nextDirection
|
|
4724
|
+
: `Work toward the goal: "${goal}". This goal must be completed. Explore, plan, and execute. Do not stop until it is done.`;
|
|
4725
|
+
nextDirection = undefined;
|
|
4726
|
+
await this.session.prompt(promptText);
|
|
4660
4727
|
this.goalRunnerRetryAttempt = 0;
|
|
4728
|
+
this.goalRunnerTurnCount++;
|
|
4729
|
+
// Track token spend from last assistant message if available
|
|
4730
|
+
const lastMsg = this.session.getLastAssistantMessage();
|
|
4731
|
+
if (lastMsg?.usage) {
|
|
4732
|
+
this.goalRunnerTokenSpend += lastMsg.usage.input + lastMsg.usage.output;
|
|
4733
|
+
}
|
|
4734
|
+
// Run evaluator to check if condition is met
|
|
4735
|
+
const model = this.session.model;
|
|
4736
|
+
if (model) {
|
|
4737
|
+
const auth = await this.session.modelRegistry.getApiKeyAndHeaders(model);
|
|
4738
|
+
if (auth.ok) {
|
|
4739
|
+
const lastText = this.session.getLastAssistantText();
|
|
4740
|
+
const evaluation = await evaluateGoalCondition(goal, lastText, model, {
|
|
4741
|
+
apiKey: auth.apiKey,
|
|
4742
|
+
headers: auth.headers,
|
|
4743
|
+
signal,
|
|
4744
|
+
});
|
|
4745
|
+
this.goalRunnerLastEvalReason = evaluation.reason;
|
|
4746
|
+
if (evaluation.met) {
|
|
4747
|
+
this.goalRunnerEnabled = false;
|
|
4748
|
+
const elapsed = this.formatDuration(Date.now() - this.goalRunnerStartTime);
|
|
4749
|
+
this.chatContainer.addChild(new Spacer(1));
|
|
4750
|
+
this.chatContainer.addChild(new Text(theme.fg("success", `Goal achieved! (${this.goalRunnerTurnCount} turns, ${elapsed})`), 1, 0));
|
|
4751
|
+
this.chatContainer.addChild(new Text(theme.fg("dim", ` ${evaluation.reason}`), 1, 0));
|
|
4752
|
+
this.ui.requestRender();
|
|
4753
|
+
break;
|
|
4754
|
+
}
|
|
4755
|
+
// Capture next direction from evaluator
|
|
4756
|
+
nextDirection = evaluation.nextDirection;
|
|
4757
|
+
// Update status with evaluator reason
|
|
4758
|
+
this.showStatus(`Goal: "${goal}" — ${evaluation.reason} (${this.goalRunnerTurnCount} turn(s), ${keyText("app.interrupt")} to pause)`);
|
|
4759
|
+
}
|
|
4760
|
+
else {
|
|
4761
|
+
nextDirection = undefined;
|
|
4762
|
+
this.showStatus(`Goal: "${goal}" — continuing (${this.goalRunnerTurnCount} turn(s), ${keyText("app.interrupt")} to pause)`);
|
|
4763
|
+
}
|
|
4764
|
+
}
|
|
4765
|
+
else {
|
|
4766
|
+
nextDirection = undefined;
|
|
4767
|
+
this.showStatus(`Goal: "${goal}" — continuing (${this.goalRunnerTurnCount} turn(s), ${keyText("app.interrupt")} to pause)`);
|
|
4768
|
+
}
|
|
4661
4769
|
}
|
|
4662
4770
|
catch (error) {
|
|
4771
|
+
// Error occurred — retry indefinitely, never stop
|
|
4663
4772
|
this.goalRunnerRetryAttempt++;
|
|
4664
4773
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
4665
|
-
|
|
4666
|
-
this.goalRunnerEnabled = false;
|
|
4667
|
-
this.showError(`Goal runner paused after ${this.GOAL_RUNNER_MAX_RETRIES} consecutive errors. ` +
|
|
4668
|
-
`Last error: ${errorMessage}. Use /goal on to resume.`);
|
|
4669
|
-
break;
|
|
4670
|
-
}
|
|
4671
|
-
// Exponential backoff
|
|
4672
|
-
const delay = this.GOAL_RUNNER_BASE_DELAY_MS * 2 ** (this.goalRunnerRetryAttempt - 1);
|
|
4673
|
-
this.showStatus(`Goal runner error, retrying in ${Math.ceil(delay / 1000)}s ` +
|
|
4674
|
-
`(${this.goalRunnerRetryAttempt}/${this.GOAL_RUNNER_MAX_RETRIES})... (${keyText("app.interrupt")} to pause)`);
|
|
4774
|
+
this.showStatus(`Goal runner error (attempt ${this.goalRunnerRetryAttempt}), retrying in ${Math.ceil(this.GOAL_RUNNER_RETRY_DELAY_MS / 1000)}s: ${errorMessage} — (${keyText("app.interrupt")} to pause)`);
|
|
4675
4775
|
try {
|
|
4676
|
-
await sleep(
|
|
4776
|
+
await sleep(this.GOAL_RUNNER_RETRY_DELAY_MS, signal);
|
|
4677
4777
|
}
|
|
4678
4778
|
catch {
|
|
4679
4779
|
break;
|