@songsid/agend 2.1.5-beta.18 → 2.1.5-beta.19
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/dist/backend/codex.d.ts +2 -1
- package/dist/backend/codex.js +33 -0
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/types.d.ts +24 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/daemon.d.ts +30 -8
- package/dist/daemon.js +175 -21
- package/dist/daemon.js.map +1 -1
- package/package.json +1 -1
package/dist/daemon.js
CHANGED
|
@@ -431,6 +431,9 @@ export class BackendUnreachableStartupError extends Error {
|
|
|
431
431
|
}
|
|
432
432
|
/** Bounded wait (under the pane lock) for the prompt to return before retrying a dropped Enter. */
|
|
433
433
|
const STRANDED_RETRY_READY_WAIT_MS = 30_000;
|
|
434
|
+
/** A passive startup phase must clear within this bound before an Enter is sent. */
|
|
435
|
+
const INPUT_TRANSIENT_WAIT_MS = 30_000;
|
|
436
|
+
const INPUT_TRANSIENT_POLL_MS = 250;
|
|
434
437
|
/** Max "stranded text → submit → wait for prompt → re-check" rounds per delivery; each may send one recovery Enter. */
|
|
435
438
|
const STRANDED_INPUT_MAX_ROUNDS = 3;
|
|
436
439
|
const FIRST_ENTER_SETTLE_MS = 1_750;
|
|
@@ -857,6 +860,14 @@ export class Daemon extends EventEmitter {
|
|
|
857
860
|
spawnSettled = null;
|
|
858
861
|
resolveSpawnSettled = null;
|
|
859
862
|
spawnDepth = 0;
|
|
863
|
+
/** Monotonic identity for nested beginSpawn/endSpawn lifecycles. */
|
|
864
|
+
spawnGeneration = 0;
|
|
865
|
+
/**
|
|
866
|
+
* Keeps passive startup-transient checks alive through the first positively
|
|
867
|
+
* submitted post-spawn message. Without this tail, a transient painted just
|
|
868
|
+
* after the startup scan can still swallow that message's Enter.
|
|
869
|
+
*/
|
|
870
|
+
inputTransientGuardGeneration = null;
|
|
860
871
|
skipResume = false;
|
|
861
872
|
startupAborted = false;
|
|
862
873
|
/** First time the current on-screen blocking dialog was seen (0 = none); drives the parked report. */
|
|
@@ -2523,6 +2534,9 @@ export class Daemon extends EventEmitter {
|
|
|
2523
2534
|
async stop() {
|
|
2524
2535
|
this.logger.info("Stopping daemon instance");
|
|
2525
2536
|
this.turnReplyGuard.reset();
|
|
2537
|
+
// Invalidate any bounded pre-Enter wait from the process generation being
|
|
2538
|
+
// stopped. It must fail closed, not press Enter in a replacement pane.
|
|
2539
|
+
this.inputTransientGuardGeneration = null;
|
|
2526
2540
|
this.freezeRuntimeMonitors();
|
|
2527
2541
|
this.pendingIpcRequests.clear();
|
|
2528
2542
|
if (this.adapter)
|
|
@@ -4046,16 +4060,24 @@ export class Daemon extends EventEmitter {
|
|
|
4046
4060
|
}
|
|
4047
4061
|
}
|
|
4048
4062
|
}
|
|
4049
|
-
// Everything above is *waiting*; everything below
|
|
4050
|
-
//
|
|
4051
|
-
//
|
|
4052
|
-
//
|
|
4063
|
+
// Everything above is *waiting*; everything below owns the write lock.
|
|
4064
|
+
// Long idle/dialog waits stay outside because the runtime dismisser may be
|
|
4065
|
+
// what makes progress. The one under-lock wait below is explicitly passive:
|
|
4066
|
+
// it clears by itself and closes a repaint race immediately before paste.
|
|
4053
4067
|
for (let round = 0;; round++) {
|
|
4054
4068
|
const outcome = await this.paneWriteLock.run(async () => {
|
|
4055
4069
|
if (cancelled())
|
|
4056
4070
|
return false;
|
|
4057
4071
|
if (this.refuseFatalStartupDelivery(status))
|
|
4058
4072
|
return false;
|
|
4073
|
+
// A passive startup phase may paint after the outer readiness probe.
|
|
4074
|
+
// It clears without input, so waiting under the pane lock cannot starve
|
|
4075
|
+
// a dialog dismisser and closes the final clear→paste TOCTOU window.
|
|
4076
|
+
if (!(await this.waitForInputTransientToClear("pre-write"))) {
|
|
4077
|
+
if (status)
|
|
4078
|
+
this.emit("message_failed", status); // ❌
|
|
4079
|
+
return false;
|
|
4080
|
+
}
|
|
4059
4081
|
// TOCTOU: the probes above ran outside this lock, and the CLI repaints
|
|
4060
4082
|
// whenever it likes — a resume prompt can be painted between "clear" and
|
|
4061
4083
|
// this critical section. Re-probe here, right before the write; a
|
|
@@ -4099,6 +4121,75 @@ export class Daemon extends EventEmitter {
|
|
|
4099
4121
|
deliveryBlockingDialogs() {
|
|
4100
4122
|
return (this.backend?.getRuntimeDialogs?.() ?? []).filter(d => d.blocksDelivery || d.holdOnly);
|
|
4101
4123
|
}
|
|
4124
|
+
/** Passive input-unavailable phases relevant to the current spawn only. */
|
|
4125
|
+
guardedInputTransients() {
|
|
4126
|
+
if (this.inputTransientGuardGeneration === null
|
|
4127
|
+
|| this.inputTransientGuardGeneration !== this.spawnGeneration)
|
|
4128
|
+
return [];
|
|
4129
|
+
return this.backend?.getInputUnavailableTransients?.() ?? [];
|
|
4130
|
+
}
|
|
4131
|
+
/** Match a passive transient against the current interactive screen. */
|
|
4132
|
+
inputTransientInPane(pane) {
|
|
4133
|
+
for (const transient of this.guardedInputTransients()) {
|
|
4134
|
+
// `pattern` is only a cheap pre-filter. `isActive` is deliberately the
|
|
4135
|
+
// authority because users and agents routinely quote diagnostic text.
|
|
4136
|
+
transient.pattern.lastIndex = 0;
|
|
4137
|
+
if (!transient.pattern.test(pane))
|
|
4138
|
+
continue;
|
|
4139
|
+
if (transient.isActive(pane))
|
|
4140
|
+
return transient;
|
|
4141
|
+
}
|
|
4142
|
+
return null;
|
|
4143
|
+
}
|
|
4144
|
+
/** Capture failure is unknown, never evidence that input is available. */
|
|
4145
|
+
async probeInputTransient() {
|
|
4146
|
+
if (this.guardedInputTransients().length === 0 || !this.tmux)
|
|
4147
|
+
return { state: "clear" };
|
|
4148
|
+
try {
|
|
4149
|
+
const pane = await this.tmux.capturePane();
|
|
4150
|
+
const transient = this.inputTransientInPane(pane);
|
|
4151
|
+
return transient ? { state: "active", transient } : { state: "clear" };
|
|
4152
|
+
}
|
|
4153
|
+
catch (err) {
|
|
4154
|
+
this.logger.debug({ err }, "capture-pane failed during the input-transient probe — pane state unknown");
|
|
4155
|
+
return { state: "unknown" };
|
|
4156
|
+
}
|
|
4157
|
+
}
|
|
4158
|
+
/**
|
|
4159
|
+
* Wait under the pane lock for a passive transient to disappear. Unlike a
|
|
4160
|
+
* dialog, this state clears by itself, so the wait cannot starve a keypress
|
|
4161
|
+
* dismisser. Stop or a replacement spawn invalidates the captured identity.
|
|
4162
|
+
*/
|
|
4163
|
+
async waitForInputTransientToClear(phase, timeoutMs = INPUT_TRANSIENT_WAIT_MS) {
|
|
4164
|
+
const generation = this.inputTransientGuardGeneration;
|
|
4165
|
+
if (generation === null || generation !== this.spawnGeneration)
|
|
4166
|
+
return true;
|
|
4167
|
+
const deadline = Date.now() + timeoutMs;
|
|
4168
|
+
let observedDescription = null;
|
|
4169
|
+
for (;;) {
|
|
4170
|
+
if (this.inputTransientGuardGeneration !== generation || this.spawnGeneration !== generation) {
|
|
4171
|
+
this.logger.warn({ phase, generation }, "Spawn changed while waiting for input availability — refusing to send Enter into the replacement pane");
|
|
4172
|
+
return false;
|
|
4173
|
+
}
|
|
4174
|
+
const probe = await this.probeInputTransient();
|
|
4175
|
+
if (this.inputTransientGuardGeneration !== generation || this.spawnGeneration !== generation) {
|
|
4176
|
+
this.logger.warn({ phase, generation }, "Spawn changed during the input-availability probe — refusing to send Enter into the replacement pane");
|
|
4177
|
+
return false;
|
|
4178
|
+
}
|
|
4179
|
+
if (probe.state === "clear")
|
|
4180
|
+
return true;
|
|
4181
|
+
if (probe.state === "active" && observedDescription !== probe.transient.description) {
|
|
4182
|
+
observedDescription = probe.transient.description;
|
|
4183
|
+
this.logger.info({ phase, transient: probe.transient.description, generation }, "CLI is still completing startup — waiting before sending Enter");
|
|
4184
|
+
}
|
|
4185
|
+
const remaining = deadline - Date.now();
|
|
4186
|
+
if (remaining <= 0) {
|
|
4187
|
+
this.logger.error({ phase, generation, timeoutMs, transient: observedDescription }, "CLI input stayed unavailable — refusing to send Enter");
|
|
4188
|
+
return false;
|
|
4189
|
+
}
|
|
4190
|
+
await new Promise(r => setTimeout(r, Math.min(INPUT_TRANSIENT_POLL_MS, remaining)));
|
|
4191
|
+
}
|
|
4192
|
+
}
|
|
4102
4193
|
static dialogKey(dialog) {
|
|
4103
4194
|
return `${dialog.pattern.source}/${dialog.pattern.flags}`;
|
|
4104
4195
|
}
|
|
@@ -4209,10 +4300,10 @@ export class Daemon extends EventEmitter {
|
|
|
4209
4300
|
}
|
|
4210
4301
|
/**
|
|
4211
4302
|
* Why a pane is not deliverable matters. "busy" may be handed to a native
|
|
4212
|
-
* input queue or steered (after its own dialog probe); "dialog"
|
|
4213
|
-
* must never be — a paste + Enter into a dialog answers it
|
|
4214
|
-
*
|
|
4215
|
-
*
|
|
4303
|
+
* input queue or steered (after its own dialog probe); "dialog", "transient"
|
|
4304
|
+
* and "unknown" must never be — a paste + Enter into a dialog answers it
|
|
4305
|
+
* with its default, while a startup transient swallows it and an unreadable
|
|
4306
|
+
* pane proves nothing. The silence gate comes first and costs no capture.
|
|
4216
4307
|
*/
|
|
4217
4308
|
async paneReadinessForDelivery(windowId) {
|
|
4218
4309
|
if (!this.isPaneIdleForDelivery(windowId))
|
|
@@ -4220,6 +4311,11 @@ export class Daemon extends EventEmitter {
|
|
|
4220
4311
|
const probe = await this.probeBlockingDialog();
|
|
4221
4312
|
if (probe.state !== "clear")
|
|
4222
4313
|
return probe.state;
|
|
4314
|
+
const transient = await this.probeInputTransient();
|
|
4315
|
+
if (transient.state === "active")
|
|
4316
|
+
return "transient";
|
|
4317
|
+
if (transient.state === "unknown")
|
|
4318
|
+
return "unknown";
|
|
4223
4319
|
if (this.backend?.dropsEnterWhileBusy?.() !== true)
|
|
4224
4320
|
return "ready";
|
|
4225
4321
|
const prompt = this.backend.getBottomReadyPattern?.();
|
|
@@ -4266,6 +4362,7 @@ export class Daemon extends EventEmitter {
|
|
|
4266
4362
|
const deadline = Date.now() + timeoutMs;
|
|
4267
4363
|
const bottomGated = this.backend?.dropsEnterWhileBusy?.() === true;
|
|
4268
4364
|
let unknownStreak = 0;
|
|
4365
|
+
let transientDeadline = 0;
|
|
4269
4366
|
for (;;) {
|
|
4270
4367
|
const remaining = deadline - Date.now();
|
|
4271
4368
|
if (remaining <= 0)
|
|
@@ -4275,6 +4372,26 @@ export class Daemon extends EventEmitter {
|
|
|
4275
4372
|
return false;
|
|
4276
4373
|
// The idle wait just resolved, so re-reading the silence gate would only
|
|
4277
4374
|
// add a stale-cache race; ask the pane the remaining questions directly.
|
|
4375
|
+
const transient = await this.probeInputTransient();
|
|
4376
|
+
if (transient.state === "active") {
|
|
4377
|
+
unknownStreak = 0;
|
|
4378
|
+
transientDeadline ||= Math.min(deadline, Date.now() + INPUT_TRANSIENT_WAIT_MS);
|
|
4379
|
+
if (Date.now() >= transientDeadline) {
|
|
4380
|
+
this.logger.error({ transient: transient.transient.description, timeoutMs: INPUT_TRANSIENT_WAIT_MS }, "CLI input stayed unavailable during the delivery-readiness wait");
|
|
4381
|
+
return false;
|
|
4382
|
+
}
|
|
4383
|
+
await new Promise(r => setTimeout(r, BOTTOM_READY_POLL_MS));
|
|
4384
|
+
continue;
|
|
4385
|
+
}
|
|
4386
|
+
transientDeadline = 0;
|
|
4387
|
+
if (transient.state === "unknown") {
|
|
4388
|
+
if (++unknownStreak >= DIALOG_PROBE_UNKNOWN_MAX) {
|
|
4389
|
+
this.logger.error({ probes: unknownStreak }, "Pane stayed unreadable during the input-transient probe — refusing to deliver blind");
|
|
4390
|
+
return false;
|
|
4391
|
+
}
|
|
4392
|
+
await new Promise(r => setTimeout(r, BOTTOM_READY_POLL_MS));
|
|
4393
|
+
continue;
|
|
4394
|
+
}
|
|
4278
4395
|
if (bottomGated) {
|
|
4279
4396
|
if (await this.isPaneReadyForDelivery(windowId))
|
|
4280
4397
|
return true;
|
|
@@ -4310,15 +4427,16 @@ export class Daemon extends EventEmitter {
|
|
|
4310
4427
|
* must fail rather than paste on top of the stranded text).
|
|
4311
4428
|
*/
|
|
4312
4429
|
async submitStrandedInputIfAny(windowId) {
|
|
4313
|
-
if (this.
|
|
4430
|
+
if (!this.tmux || !this.backend)
|
|
4314
4431
|
return "idle";
|
|
4315
4432
|
const prompt = this.backend.getBottomReadyPattern?.();
|
|
4316
4433
|
if (!prompt)
|
|
4317
4434
|
return "idle";
|
|
4435
|
+
const requireBottomReady = this.backend.dropsEnterWhileBusy?.() === true;
|
|
4318
4436
|
// Pre-check outside the lock so the common case (verified clear) never
|
|
4319
4437
|
// contends with the dialog dismisser. Only a POSITIVE "clear" skips the
|
|
4320
4438
|
// lock; an unreadable pane is re-examined under it, not waved through.
|
|
4321
|
-
const pre = await this.strandedInputState(prompt);
|
|
4439
|
+
const pre = await this.strandedInputState(prompt, requireBottomReady);
|
|
4322
4440
|
if (pre === "clear")
|
|
4323
4441
|
return "idle";
|
|
4324
4442
|
if (pre === "busy")
|
|
@@ -4334,7 +4452,7 @@ export class Daemon extends EventEmitter {
|
|
|
4334
4452
|
// no-op at an empty prompt, but into a modal it is a confirmation.
|
|
4335
4453
|
if (this.fatalStartupBlocked)
|
|
4336
4454
|
return "busy";
|
|
4337
|
-
const state = await this.strandedInputState(prompt);
|
|
4455
|
+
const state = await this.strandedInputState(prompt, requireBottomReady);
|
|
4338
4456
|
if (state === "clear")
|
|
4339
4457
|
return "idle";
|
|
4340
4458
|
if (state !== "stranded")
|
|
@@ -4344,12 +4462,12 @@ export class Daemon extends EventEmitter {
|
|
|
4344
4462
|
});
|
|
4345
4463
|
}
|
|
4346
4464
|
/**
|
|
4347
|
-
* "stranded": an AgEnD message sits in the input row; "clear":
|
|
4348
|
-
*
|
|
4349
|
-
*
|
|
4350
|
-
* read. Only "clear"
|
|
4465
|
+
* "stranded": an AgEnD message sits in the input row; "clear": no strand is
|
|
4466
|
+
* visible under the backend's readiness policy; "busy": generating, or (for
|
|
4467
|
+
* a bottom-gated backend) the prompt is not at the bottom; "unknown": the
|
|
4468
|
+
* pane could not be read. Only "clear" permits the next paste.
|
|
4351
4469
|
*/
|
|
4352
|
-
async strandedInputState(prompt) {
|
|
4470
|
+
async strandedInputState(prompt, requireBottomReady) {
|
|
4353
4471
|
let pane;
|
|
4354
4472
|
try {
|
|
4355
4473
|
pane = await this.tmux.capturePane();
|
|
@@ -4357,11 +4475,16 @@ export class Daemon extends EventEmitter {
|
|
|
4357
4475
|
catch {
|
|
4358
4476
|
return "unknown";
|
|
4359
4477
|
}
|
|
4478
|
+
if (this.inputTransientInPane(pane))
|
|
4479
|
+
return "busy";
|
|
4360
4480
|
if (this.backend?.getBusyPattern?.()?.test(pane))
|
|
4361
4481
|
return "busy";
|
|
4362
4482
|
if (strandedAgendMessageInInput(pane, prompt))
|
|
4363
4483
|
return "stranded";
|
|
4364
|
-
|
|
4484
|
+
// Kiro needs a bottom-anchored prompt as positive readiness evidence. Codex
|
|
4485
|
+
// does not: this preflight runs only after its ordinary silence gate and is
|
|
4486
|
+
// enabled solely to recover a positively identified old AgEnD strand.
|
|
4487
|
+
return !requireBottomReady || bottomRowIsReady(pane, prompt) ? "clear" : "busy";
|
|
4365
4488
|
}
|
|
4366
4489
|
/**
|
|
4367
4490
|
* F2: for Enter-dropping TUIs, "some output after Enter" proves nothing — the
|
|
@@ -4465,6 +4588,8 @@ export class Daemon extends EventEmitter {
|
|
|
4465
4588
|
* an invariant maintained by comments.
|
|
4466
4589
|
*/
|
|
4467
4590
|
async sendDeliveryEnter(phase) {
|
|
4591
|
+
if (!(await this.waitForInputTransientToClear(phase)))
|
|
4592
|
+
return false;
|
|
4468
4593
|
const sent = await this.tmux.sendSpecialKey("Enter");
|
|
4469
4594
|
if (!sent) {
|
|
4470
4595
|
this.logger.error({
|
|
@@ -4759,6 +4884,13 @@ export class Daemon extends EventEmitter {
|
|
|
4759
4884
|
canProveSubmission() {
|
|
4760
4885
|
return !!this.backend?.getBottomReadyPattern?.();
|
|
4761
4886
|
}
|
|
4887
|
+
/** Positive submission retires the startup transient guard for this spawn. */
|
|
4888
|
+
submittedProof() {
|
|
4889
|
+
if (this.inputTransientGuardGeneration === this.spawnGeneration) {
|
|
4890
|
+
this.inputTransientGuardGeneration = null;
|
|
4891
|
+
}
|
|
4892
|
+
return "submitted";
|
|
4893
|
+
}
|
|
4762
4894
|
/**
|
|
4763
4895
|
* Decide a submission after an Enter, with one bounded retry.
|
|
4764
4896
|
*
|
|
@@ -4852,16 +4984,16 @@ export class Daemon extends EventEmitter {
|
|
|
4852
4984
|
// failure to read the pane BEFORE pasting cannot turn a delivered
|
|
4853
4985
|
// message into a re-paste.
|
|
4854
4986
|
if (signature.unique && after.payload > 0)
|
|
4855
|
-
return
|
|
4987
|
+
return this.submittedProof();
|
|
4856
4988
|
// 3. Otherwise the evidence must be NEW relative to the pane as it was
|
|
4857
4989
|
// before we pasted: a queue marker left by an earlier message, or an
|
|
4858
4990
|
// older transcript entry that opens the same way, are on screen either
|
|
4859
4991
|
// way and would otherwise confirm a paste that never landed.
|
|
4860
4992
|
if (baseline) {
|
|
4861
4993
|
if (after.queued > baseline.queued)
|
|
4862
|
-
return
|
|
4994
|
+
return this.submittedProof();
|
|
4863
4995
|
if (after.payload > baseline.payload)
|
|
4864
|
-
return
|
|
4996
|
+
return this.submittedProof();
|
|
4865
4997
|
// 4. Nothing new of ours anywhere: the paste was swallowed by a redraw.
|
|
4866
4998
|
return "unproven";
|
|
4867
4999
|
}
|
|
@@ -5510,6 +5642,12 @@ export class Daemon extends EventEmitter {
|
|
|
5510
5642
|
// warning is safer than treating the replacement's startup idle as the old
|
|
5511
5643
|
// turn ending and injecting a stale recovery prompt.
|
|
5512
5644
|
this.turnReplyGuard.reset();
|
|
5645
|
+
if (this.spawnDepth === 0) {
|
|
5646
|
+
this.spawnGeneration++;
|
|
5647
|
+
this.inputTransientGuardGeneration = (this.backend?.getInputUnavailableTransients?.().length ?? 0) > 0
|
|
5648
|
+
? this.spawnGeneration
|
|
5649
|
+
: null;
|
|
5650
|
+
}
|
|
5513
5651
|
this.spawnDepth++;
|
|
5514
5652
|
this.spawning = true;
|
|
5515
5653
|
if (!this.spawnSettled) {
|
|
@@ -6109,6 +6247,7 @@ export class Daemon extends EventEmitter {
|
|
|
6109
6247
|
await new Promise(r => setTimeout(r, Math.min(pollMs, Math.max(remaining(), 0)))); };
|
|
6110
6248
|
let cleanReadyPolls = 0;
|
|
6111
6249
|
let lastDialog = null;
|
|
6250
|
+
let lastTransient = null;
|
|
6112
6251
|
let captureFailures = 0;
|
|
6113
6252
|
let attempts = 0;
|
|
6114
6253
|
do {
|
|
@@ -6130,6 +6269,18 @@ export class Daemon extends EventEmitter {
|
|
|
6130
6269
|
continue;
|
|
6131
6270
|
}
|
|
6132
6271
|
try {
|
|
6272
|
+
// Some TUIs expose a real-looking input row before they accept Enter.
|
|
6273
|
+
// This phase clears by itself: never send a key, and let its structural
|
|
6274
|
+
// current-screen matcher outrank the otherwise-valid ready pattern.
|
|
6275
|
+
const transient = this.inputTransientInPane(pane);
|
|
6276
|
+
if (transient) {
|
|
6277
|
+
cleanReadyPolls = 0;
|
|
6278
|
+
lastDialog = null;
|
|
6279
|
+
lastTransient = transient;
|
|
6280
|
+
await sleep();
|
|
6281
|
+
continue;
|
|
6282
|
+
}
|
|
6283
|
+
lastTransient = null;
|
|
6133
6284
|
// Try each startup dialog pattern before checking ready state
|
|
6134
6285
|
let matched = false;
|
|
6135
6286
|
lastDialog = null;
|
|
@@ -6257,7 +6408,10 @@ export class Daemon extends EventEmitter {
|
|
|
6257
6408
|
await sleep();
|
|
6258
6409
|
} while (remaining() > 0);
|
|
6259
6410
|
// Budget exhausted. Never silently: say which way it went.
|
|
6260
|
-
if (
|
|
6411
|
+
if (lastTransient) {
|
|
6412
|
+
this.logger.warn({ description: lastTransient.description, attempts, budgetMs }, "Startup scan exhausted while CLI input was still unavailable — retaining the delivery guard");
|
|
6413
|
+
}
|
|
6414
|
+
else if (lastDialog) {
|
|
6261
6415
|
this.logger.warn({ description: lastDialog.description, attempts, budgetMs }, "Startup scan exhausted with a dialog still on screen — reporting the CLI alive; deliveries stay blocked until it is answered");
|
|
6262
6416
|
}
|
|
6263
6417
|
else {
|