@gajae-code/coding-agent 0.12.5 → 0.12.7
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 +34 -0
- package/README.md +4 -1
- package/dist/types/async/job-manager.d.ts +36 -1
- package/dist/types/extensibility/extensions/runner.d.ts +9 -7
- package/dist/types/gjc-runtime/launch-tmux.d.ts +22 -3
- package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
- package/dist/types/gjc-runtime/team-launch.d.ts +3 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +14 -2
- package/dist/types/gjc-runtime/team-store.d.ts +0 -4
- package/dist/types/gjc-runtime/team-workers.d.ts +5 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-provider-context.d.ts +57 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +8 -1
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +3 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +21 -4
- package/dist/types/modes/components/gjc-bundle-settings.d.ts +1 -0
- package/dist/types/modes/components/plugin-settings.d.ts +2 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/sdk/broker/broker.d.ts +18 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +13 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -3
- package/dist/types/sdk/bus/index.d.ts +4 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +2 -2
- package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -3
- package/dist/types/sdk/bus/topic-registry.d.ts +63 -3
- package/dist/types/session/agent-session.d.ts +5 -4
- package/dist/types/session/attempt-record-store.d.ts +19 -0
- package/dist/types/session/session-manager.d.ts +1 -0
- package/dist/types/session/session-storage.d.ts +8 -0
- package/dist/types/task/receipt.d.ts +1 -0
- package/dist/types/task/types.d.ts +43 -0
- package/dist/types/tools/subagent.d.ts +15 -0
- package/dist/types/tools/todo-write.d.ts +3 -0
- package/package.json +8 -8
- package/scripts/capture-sticky-viewport-showcase.ts +131 -5
- package/scripts/verify-gjc-sdk-canonicalization.ts +128 -183
- package/scripts/verify-sticky-viewport-showcase.ts +620 -93
- package/src/async/job-manager.ts +430 -46
- package/src/config/model-profiles.ts +8 -0
- package/src/config/settings-schema.ts +17 -3
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -0
- package/src/extensibility/extensions/runner.ts +80 -6
- package/src/extensibility/extensions/wrapper.ts +22 -15
- package/src/extensibility/runtime-skill-discovery.ts +4 -6
- package/src/gjc-runtime/launch-tmux.ts +309 -293
- package/src/gjc-runtime/managed-owner-supervisor.ts +6 -2
- package/src/gjc-runtime/psmux-detect.ts +52 -6
- package/src/gjc-runtime/team-launch.ts +50 -2
- package/src/gjc-runtime/team-runtime.ts +716 -251
- package/src/gjc-runtime/team-store.ts +25 -7
- package/src/gjc-runtime/team-workers.ts +161 -1
- package/src/gjc-runtime/tmux-common.ts +16 -4
- package/src/gjc-runtime/tmux-gc.ts +37 -5
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -0
- package/src/gjc-runtime/tmux-provider-context.ts +451 -0
- package/src/gjc-runtime/tmux-sessions.ts +409 -156
- package/src/gjc-runtime/ultragoal-change-set.ts +90 -44
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +7 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +360 -156
- package/src/internal-urls/docs-index.generated.ts +4 -4
- package/src/internal-urls/local-protocol.ts +39 -18
- package/src/modes/acp/acp-agent.ts +27 -13
- package/src/modes/components/gjc-bundle-settings.ts +2 -0
- package/src/modes/components/plugin-settings.ts +5 -0
- package/src/modes/components/settings-selector.ts +4 -0
- package/src/modes/controllers/selector-controller.ts +1 -0
- package/src/prompts/tools/monitor.md +4 -4
- package/src/sdk/broker/broker.ts +93 -10
- package/src/sdk/broker/lifecycle-ledger.ts +227 -11
- package/src/sdk/broker/lifecycle.ts +691 -92
- package/src/sdk/bus/chat-daemon-control.ts +30 -6
- package/src/sdk/bus/index.ts +23 -6
- package/src/sdk/bus/telegram-daemon-contract.ts +2 -2
- package/src/sdk/bus/telegram-daemon.ts +283 -44
- package/src/sdk/bus/topic-registry.ts +133 -10
- package/src/sdk/session.ts +7 -6
- package/src/session/agent-session.ts +752 -339
- package/src/session/attempt-record-store.ts +106 -0
- package/src/session/internal/managed-session-scope.ts +252 -67
- package/src/session/internal/managed-session-storage.ts +1 -0
- package/src/session/session-manager.ts +7 -0
- package/src/session/session-storage.ts +146 -24
- package/src/task/index.ts +230 -19
- package/src/task/provider-retry-status.ts +4 -1
- package/src/task/receipt.ts +2 -0
- package/src/task/types.ts +10 -0
- package/src/tools/monitor.ts +7 -1
- package/src/tools/output-meta.ts +10 -2
- package/src/tools/subagent.ts +97 -75
- package/src/tools/todo-write.ts +47 -0
|
@@ -94,6 +94,18 @@ export interface TopicDeleteAuthoritySnapshot {
|
|
|
94
94
|
record?: TopicRecord;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Proof that a definite delete was settled in memory and still owes its durable
|
|
99
|
+
* commit. Handed back only by a settlement that actually happened, so a refused
|
|
100
|
+
* settlement structurally cannot be followed by a rollback.
|
|
101
|
+
*/
|
|
102
|
+
export interface TopicSettledDelete {
|
|
103
|
+
sessionId: string;
|
|
104
|
+
topicId: string;
|
|
105
|
+
/** Authority epoch proven current when the record was removed. */
|
|
106
|
+
settledEpoch: number;
|
|
107
|
+
}
|
|
108
|
+
|
|
97
109
|
function isValidBindingString(value: unknown): value is string {
|
|
98
110
|
return typeof value === "string" && value.trim().length > 0;
|
|
99
111
|
}
|
|
@@ -136,6 +148,16 @@ function isValidTopicId(value: unknown): value is string {
|
|
|
136
148
|
);
|
|
137
149
|
}
|
|
138
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Monotonic epoch successor that saturates instead of leaving the safe-integer
|
|
153
|
+
* range. Past `Number.MAX_SAFE_INTEGER` two distinct generations collapse onto
|
|
154
|
+
* the same IEEE-754 double, which would let a stale settlement clear a newer
|
|
155
|
+
* fence; settlement refuses the saturated value instead of overflowing past it.
|
|
156
|
+
*/
|
|
157
|
+
function nextAuthorityEpoch(current: number): number {
|
|
158
|
+
return current >= Number.MAX_SAFE_INTEGER ? Number.MAX_SAFE_INTEGER : current + 1;
|
|
159
|
+
}
|
|
160
|
+
|
|
139
161
|
export function emptyTopicRegistryState(): TopicRegistryState {
|
|
140
162
|
return { topics: {} };
|
|
141
163
|
}
|
|
@@ -161,6 +183,8 @@ export class TopicRegistry {
|
|
|
161
183
|
private readonly creatingBindings = new Map<string, TopicEndpointBinding>();
|
|
162
184
|
/** Monotonic authority epochs, including deletion fences for absent records. */
|
|
163
185
|
private readonly epochs = new Map<string, number>();
|
|
186
|
+
/** Phase-1 settlements awaiting their durable clear; their topic ids stay quarantined. */
|
|
187
|
+
readonly #settling = new Map<string, { topicId: string; settledEpoch: number; record: TopicRecord }>();
|
|
164
188
|
|
|
165
189
|
constructor(state: TopicRegistryState = emptyTopicRegistryState()) {
|
|
166
190
|
this.topics = new Map();
|
|
@@ -173,6 +197,7 @@ export class TopicRegistry {
|
|
|
173
197
|
this.byTopic.clear();
|
|
174
198
|
this.#ambiguousTopicIds.clear();
|
|
175
199
|
this.epochs.clear();
|
|
200
|
+
this.#settling.clear();
|
|
176
201
|
this.load(state);
|
|
177
202
|
}
|
|
178
203
|
|
|
@@ -259,6 +284,10 @@ export class TopicRegistry {
|
|
|
259
284
|
private rebuildInboundRoutes(): void {
|
|
260
285
|
this.byTopic.clear();
|
|
261
286
|
this.#ambiguousTopicIds.clear();
|
|
287
|
+
// A settled-but-not-yet-durable clear keeps its topic id quarantined: the
|
|
288
|
+
// clear is not authoritative until persisted, so no colliding survivor may
|
|
289
|
+
// become routable and no settled id may become adoptable during that write.
|
|
290
|
+
for (const settling of this.#settling.values()) this.#ambiguousTopicIds.add(settling.topicId);
|
|
262
291
|
const activeByTopic = new Map<string, string>();
|
|
263
292
|
|
|
264
293
|
for (const [sessionId, record] of this.topics) {
|
|
@@ -278,6 +307,13 @@ export class TopicRegistry {
|
|
|
278
307
|
}
|
|
279
308
|
}
|
|
280
309
|
|
|
310
|
+
/** Advance and publish the session authority epoch, saturating at the safe-integer bound. */
|
|
311
|
+
#advanceAuthorityEpoch(sessionId: string, base: number): number {
|
|
312
|
+
const epoch = nextAuthorityEpoch(base);
|
|
313
|
+
this.epochs.set(sessionId, epoch);
|
|
314
|
+
return epoch;
|
|
315
|
+
}
|
|
316
|
+
|
|
281
317
|
/** Resolve the owning session for a topic id (for fail-closed inbound routing). */
|
|
282
318
|
sessionForTopic(topicId: string): string | undefined {
|
|
283
319
|
return this.byTopic.get(topicId);
|
|
@@ -482,6 +518,7 @@ export class TopicRegistry {
|
|
|
482
518
|
const pending = this.inflight.get(sessionId);
|
|
483
519
|
if (pending) return pending;
|
|
484
520
|
const epoch = this.epochs.get(sessionId) ?? 0;
|
|
521
|
+
if (epoch >= Number.MAX_SAFE_INTEGER) throw new Error("topic authority epoch exhausted");
|
|
485
522
|
// Publish the compatible endpoint claim before invoking `create`: the callback
|
|
486
523
|
// may immediately begin a remote create and identity-less recovery must never
|
|
487
524
|
// observe a false absence during that await.
|
|
@@ -676,7 +713,9 @@ export class TopicRegistry {
|
|
|
676
713
|
/** Restore a failed delete fence only while its exact authority mutation remains current. */
|
|
677
714
|
restoreDeleteAuthority(snapshot: TopicDeleteAuthoritySnapshot): boolean {
|
|
678
715
|
const record = this.topics.get(snapshot.sessionId);
|
|
679
|
-
const
|
|
716
|
+
const authorityBase = Math.max(snapshot.fenceEpoch ?? 0, snapshot.authorityEpoch ?? 0);
|
|
717
|
+
if (authorityBase >= Number.MAX_SAFE_INTEGER) return false;
|
|
718
|
+
const deleteEpoch = nextAuthorityEpoch(authorityBase);
|
|
680
719
|
if (this.epochs.get(snapshot.sessionId) !== deleteEpoch) return false;
|
|
681
720
|
if (snapshot.topicId === undefined) {
|
|
682
721
|
if (record) return false;
|
|
@@ -697,10 +736,17 @@ export class TopicRegistry {
|
|
|
697
736
|
return true;
|
|
698
737
|
}
|
|
699
738
|
|
|
700
|
-
/**
|
|
739
|
+
/**
|
|
740
|
+
* Restore the exact delete fence after a failed compensation publication.
|
|
741
|
+
*
|
|
742
|
+
* Settlement rebuilds derived routes, which can make a surviving colliding
|
|
743
|
+
* record routable. Rebuild again on successful restoration so the reinstated
|
|
744
|
+
* fence re-quarantines the topic id instead of leaving inbound routing open to
|
|
745
|
+
* the collision partner despite the restored fence.
|
|
746
|
+
*/
|
|
701
747
|
restoreDeleteFence(snapshot: TopicDeleteAuthoritySnapshot): boolean {
|
|
702
748
|
const record = this.topics.get(snapshot.sessionId);
|
|
703
|
-
const deleteEpoch = Math.max(snapshot.fenceEpoch ?? 0, snapshot.authorityEpoch ?? 0)
|
|
749
|
+
const deleteEpoch = nextAuthorityEpoch(Math.max(snapshot.fenceEpoch ?? 0, snapshot.authorityEpoch ?? 0));
|
|
704
750
|
if (snapshot.topicId === undefined) {
|
|
705
751
|
if (record) return false;
|
|
706
752
|
} else if (!record) {
|
|
@@ -715,17 +761,19 @@ export class TopicRegistry {
|
|
|
715
761
|
} else {
|
|
716
762
|
record.authorityEpoch = deleteEpoch;
|
|
717
763
|
record.authorityState = "delete_pending";
|
|
718
|
-
if (this.byTopic.get(record.topicId) === snapshot.sessionId) this.byTopic.delete(record.topicId);
|
|
719
764
|
}
|
|
720
765
|
this.epochs.set(snapshot.sessionId, deleteEpoch);
|
|
766
|
+
this.rebuildInboundRoutes();
|
|
721
767
|
return true;
|
|
722
768
|
}
|
|
723
769
|
|
|
724
770
|
/** Fence new work before the remote delete starts, including an absent in-flight create. */
|
|
725
771
|
beginDelete(sessionId: string): TopicRecord | undefined {
|
|
726
772
|
const record = this.topics.get(sessionId);
|
|
727
|
-
const epoch =
|
|
728
|
-
|
|
773
|
+
const epoch = this.#advanceAuthorityEpoch(
|
|
774
|
+
sessionId,
|
|
775
|
+
Math.max(this.epochs.get(sessionId) ?? 0, record?.authorityEpoch ?? 0),
|
|
776
|
+
);
|
|
729
777
|
if (!record) return undefined;
|
|
730
778
|
record.authorityEpoch = epoch;
|
|
731
779
|
record.authorityState = "delete_pending";
|
|
@@ -799,11 +847,86 @@ export class TopicRegistry {
|
|
|
799
847
|
await this.inflight.get(sessionId)?.catch(() => undefined);
|
|
800
848
|
}
|
|
801
849
|
|
|
802
|
-
/**
|
|
803
|
-
|
|
850
|
+
/**
|
|
851
|
+
* Phase 1 of settling a definite remote delete: remove the record while
|
|
852
|
+
* deliberately RETAINING its topic-id quarantine.
|
|
853
|
+
*
|
|
854
|
+
* `dispatchedAuthorityEpoch` is the authority epoch the caller held when it
|
|
855
|
+
* dispatched the remote delete. Settlement requires that epoch to still equal
|
|
856
|
+
* both the record's own authority epoch and the registry's current epoch for
|
|
857
|
+
* the session, so a held earlier delete can never settle a newer fence: if a
|
|
858
|
+
* scan or close-started delete re-fenced the same session/topic after this
|
|
859
|
+
* delete was dispatched, the stale definite result is refused and the newer
|
|
860
|
+
* `delete_pending` record plus its topic-id quarantine stay intact.
|
|
861
|
+
*
|
|
862
|
+
* An epoch outside the safe-integer range cannot identify a generation, and at
|
|
863
|
+
* `Number.MAX_SAFE_INTEGER` epoch advancement has saturated so distinct
|
|
864
|
+
* generations are no longer distinguishable. Both fail closed: settlement can
|
|
865
|
+
* no longer be proven fresh, so the fence is kept.
|
|
866
|
+
*
|
|
867
|
+
* Derived routing tables are deliberately NOT rebuilt here. Until the cleared
|
|
868
|
+
* snapshot is durably persisted the clear is not authoritative, so publishing
|
|
869
|
+
* routes now would make a colliding survivor routable (and the settled id
|
|
870
|
+
* adoptable) during the held write, and a failed persist would re-quarantine
|
|
871
|
+
* too late. Publish with {@link commitSettledDelete} once the persist
|
|
872
|
+
* succeeds, or undo with {@link rollbackSettledDelete} when it fails.
|
|
873
|
+
*/
|
|
874
|
+
settleDelete(sessionId: string, topicId: string, dispatchedAuthorityEpoch: number): TopicSettledDelete | undefined {
|
|
875
|
+
if (!Number.isSafeInteger(dispatchedAuthorityEpoch) || dispatchedAuthorityEpoch < 0) return undefined;
|
|
876
|
+
if (this.#settling.has(sessionId)) return undefined;
|
|
804
877
|
const record = this.topics.get(sessionId);
|
|
805
|
-
if (!record || record.topicId !== topicId || record.authorityState !== "delete_pending") return
|
|
878
|
+
if (!record || record.topicId !== topicId || record.authorityState !== "delete_pending") return undefined;
|
|
879
|
+
if ((record.authorityEpoch ?? 0) !== dispatchedAuthorityEpoch) return undefined;
|
|
880
|
+
const currentEpoch = this.authorityEpoch(sessionId);
|
|
881
|
+
if (currentEpoch !== dispatchedAuthorityEpoch) return undefined;
|
|
882
|
+
if (currentEpoch >= Number.MAX_SAFE_INTEGER) return undefined;
|
|
806
883
|
this.topics.delete(sessionId);
|
|
884
|
+
this.#settling.set(sessionId, { topicId, settledEpoch: dispatchedAuthorityEpoch, record: { ...record } });
|
|
885
|
+
this.#ambiguousTopicIds.add(topicId);
|
|
886
|
+
if (this.byTopic.get(topicId) === sessionId) this.byTopic.delete(topicId);
|
|
887
|
+
return { sessionId, topicId, settledEpoch: dispatchedAuthorityEpoch };
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* Phase 2: publish derived routing tables once the cleared state is durable.
|
|
892
|
+
*
|
|
893
|
+
* Only here does the settled topic id lose its quarantine, so a surviving
|
|
894
|
+
* colliding record becomes routable and a settled id becomes adoptable without
|
|
895
|
+
* waiting for a daemon restart.
|
|
896
|
+
*/
|
|
897
|
+
commitSettledDelete(settled: TopicSettledDelete): boolean {
|
|
898
|
+
const pending = this.#settling.get(settled.sessionId);
|
|
899
|
+
if (!pending || pending.topicId !== settled.topicId || pending.settledEpoch !== settled.settledEpoch)
|
|
900
|
+
return false;
|
|
901
|
+
this.#settling.delete(settled.sessionId);
|
|
902
|
+
this.rebuildInboundRoutes();
|
|
903
|
+
return true;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* Compare-and-swap undo for a settlement whose durable clear failed.
|
|
908
|
+
*
|
|
909
|
+
* Restoration applies only while the registry still holds exactly the state
|
|
910
|
+
* that this settlement produced: the settlement is still awaiting commit, the
|
|
911
|
+
* session still has no record, and the session epoch is still the settled
|
|
912
|
+
* epoch. Any mismatch means a newer generation intervened, so the restore is
|
|
913
|
+
* refused and the newer state is left untouched. A refused settlement produced
|
|
914
|
+
* no token, so it can never reach this path.
|
|
915
|
+
*/
|
|
916
|
+
rollbackSettledDelete(settled: TopicSettledDelete): boolean {
|
|
917
|
+
const pending = this.#settling.get(settled.sessionId);
|
|
918
|
+
if (!pending || pending.topicId !== settled.topicId || pending.settledEpoch !== settled.settledEpoch)
|
|
919
|
+
return false;
|
|
920
|
+
if (this.topics.has(settled.sessionId)) return false;
|
|
921
|
+
if (this.authorityEpoch(settled.sessionId) !== settled.settledEpoch) return false;
|
|
922
|
+
this.#settling.delete(settled.sessionId);
|
|
923
|
+
this.topics.set(settled.sessionId, {
|
|
924
|
+
...pending.record,
|
|
925
|
+
authorityEpoch: settled.settledEpoch,
|
|
926
|
+
authorityState: "delete_pending",
|
|
927
|
+
});
|
|
928
|
+
this.epochs.set(settled.sessionId, settled.settledEpoch);
|
|
929
|
+
this.rebuildInboundRoutes();
|
|
807
930
|
return true;
|
|
808
931
|
}
|
|
809
932
|
|
|
@@ -811,7 +934,7 @@ export class TopicRegistry {
|
|
|
811
934
|
delete(sessionId: string): boolean {
|
|
812
935
|
const record = this.topics.get(sessionId);
|
|
813
936
|
if (!record) return false;
|
|
814
|
-
this
|
|
937
|
+
this.#advanceAuthorityEpoch(sessionId, Math.max(this.epochs.get(sessionId) ?? 0, record.authorityEpoch ?? 0));
|
|
815
938
|
if (this.byTopic.get(record.topicId) === sessionId) this.byTopic.delete(record.topicId);
|
|
816
939
|
return this.topics.delete(sessionId);
|
|
817
940
|
}
|
package/src/sdk/session.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "@gajae-code/agent-core";
|
|
12
12
|
import {
|
|
13
13
|
type AssistantMessage,
|
|
14
|
+
type AttemptScopeRef,
|
|
14
15
|
type AuthCredentialSelector,
|
|
15
16
|
type CredentialDisabledEvent,
|
|
16
17
|
type Message,
|
|
@@ -2538,21 +2539,21 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2538
2539
|
if (!obfuscator?.hasSecrets()) return converted;
|
|
2539
2540
|
return obfuscateMessages(obfuscator, converted);
|
|
2540
2541
|
};
|
|
2541
|
-
const transformContext = async (messages: AgentMessage[], _signal?: AbortSignal) => {
|
|
2542
|
+
const transformContext = async (messages: AgentMessage[], _signal?: AbortSignal, scope?: AttemptScopeRef) => {
|
|
2542
2543
|
// External Agent events dispatch listeners without awaiting them. The
|
|
2543
2544
|
// session-owned barrier makes any pre-admission artifact transformation
|
|
2544
2545
|
// visible before this provider context is normalized.
|
|
2545
2546
|
await session?.awaitPendingContextTransformations();
|
|
2546
|
-
return extensionRunner ? await extensionRunner.emitContext(messages) : messages;
|
|
2547
|
+
return extensionRunner ? await extensionRunner.emitContext(messages, scope) : messages;
|
|
2547
2548
|
};
|
|
2548
2549
|
const onPayload = extensionRunner
|
|
2549
|
-
? async (payload: unknown, _model?: Model) => {
|
|
2550
|
-
return await extensionRunner.emitBeforeProviderRequest(payload);
|
|
2550
|
+
? async (payload: unknown, _model?: Model, scope?: AttemptScopeRef) => {
|
|
2551
|
+
return await extensionRunner.emitBeforeProviderRequest(payload, scope);
|
|
2551
2552
|
}
|
|
2552
2553
|
: undefined;
|
|
2553
2554
|
const onResponse: SimpleStreamOptions["onResponse"] | undefined = extensionRunner
|
|
2554
|
-
? async (response, model) => {
|
|
2555
|
-
await extensionRunner.emitAfterProviderResponse(response, model);
|
|
2555
|
+
? async (response, model, scope) => {
|
|
2556
|
+
await extensionRunner.emitAfterProviderResponse(response, model, scope);
|
|
2556
2557
|
}
|
|
2557
2558
|
: undefined;
|
|
2558
2559
|
|