viveworker 0.8.2 → 0.8.3
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/README.md +5 -4
- package/package.json +1 -1
- package/scripts/com.viveworker.moltbook-scout.plist.sample +2 -2
- package/scripts/moltbook-api.mjs +178 -2
- package/scripts/moltbook-cli.mjs +134 -8
- package/scripts/moltbook-scout-auto.sh +100 -10
- package/scripts/moltbook-watcher.mjs +10 -0
- package/scripts/viveworker-bridge.mjs +716 -38
- package/web/app.css +33 -0
- package/web/app.js +78 -18
- package/web/build-id.js +1 -1
- package/web/i18n.js +24 -2
- package/web/remote-pairing/api-router.js +168 -15
package/web/app.css
CHANGED
|
@@ -2755,6 +2755,39 @@ pre code {
|
|
|
2755
2755
|
color: rgba(236, 248, 255, 0.66);
|
|
2756
2756
|
}
|
|
2757
2757
|
|
|
2758
|
+
.timeline-entry--activity {
|
|
2759
|
+
cursor: default;
|
|
2760
|
+
border-color: rgba(121, 196, 255, 0.18);
|
|
2761
|
+
background:
|
|
2762
|
+
linear-gradient(180deg, rgba(121, 196, 255, 0.11), rgba(20, 31, 40, 0.94)),
|
|
2763
|
+
var(--card);
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
.timeline-entry__activity-pulse {
|
|
2767
|
+
width: 0.58rem;
|
|
2768
|
+
height: 0.58rem;
|
|
2769
|
+
flex: 0 0 auto;
|
|
2770
|
+
border-radius: 999px;
|
|
2771
|
+
background: rgba(121, 196, 255, 0.9);
|
|
2772
|
+
box-shadow: 0 0 0 0 rgba(121, 196, 255, 0.26);
|
|
2773
|
+
animation: timeline-activity-pulse 1.6s ease-out infinite;
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2776
|
+
@keyframes timeline-activity-pulse {
|
|
2777
|
+
0% {
|
|
2778
|
+
box-shadow: 0 0 0 0 rgba(121, 196, 255, 0.26);
|
|
2779
|
+
opacity: 0.95;
|
|
2780
|
+
}
|
|
2781
|
+
70% {
|
|
2782
|
+
box-shadow: 0 0 0 0.56rem rgba(121, 196, 255, 0);
|
|
2783
|
+
opacity: 0.72;
|
|
2784
|
+
}
|
|
2785
|
+
100% {
|
|
2786
|
+
box-shadow: 0 0 0 0 rgba(121, 196, 255, 0);
|
|
2787
|
+
opacity: 0.95;
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2758
2791
|
.timeline-entry--message .timeline-entry__title {
|
|
2759
2792
|
font-size: 1.03rem;
|
|
2760
2793
|
line-height: 1.55;
|
package/web/app.js
CHANGED
|
@@ -897,7 +897,7 @@ async function forceAppRefreshFromLan() {
|
|
|
897
897
|
const keys = await caches.keys();
|
|
898
898
|
await Promise.all(
|
|
899
899
|
keys
|
|
900
|
-
.filter((key) => /^viveworker
|
|
900
|
+
.filter((key) => /^viveworker-/u.test(key))
|
|
901
901
|
.map((key) => caches.delete(key))
|
|
902
902
|
);
|
|
903
903
|
}
|
|
@@ -1930,17 +1930,26 @@ function currentTimelineKindFilterOption() {
|
|
|
1930
1930
|
|
|
1931
1931
|
function timelineEntryMatchesKindFilter(entry, filterId) {
|
|
1932
1932
|
const kind = normalizeClientText(entry?.kind || entry?.item?.kind || "");
|
|
1933
|
+
const fileEventType = normalizeClientText(entry?.fileEventType || entry?.item?.fileEventType || "");
|
|
1934
|
+
const activityPhase = normalizeClientText(entry?.activityPhase || entry?.item?.activityPhase || "");
|
|
1933
1935
|
switch (filterId) {
|
|
1934
1936
|
case "messages":
|
|
1935
|
-
return TIMELINE_MESSAGE_KINDS.has(kind);
|
|
1937
|
+
return TIMELINE_MESSAGE_KINDS.has(kind) || (kind === "activity_status" && activityPhase === "thinking");
|
|
1936
1938
|
case "suggestions":
|
|
1937
1939
|
return kind === "ambient_suggestions";
|
|
1938
1940
|
case "files":
|
|
1939
|
-
return
|
|
1941
|
+
return (
|
|
1942
|
+
(kind === "file_event" && fileEventType !== "git") ||
|
|
1943
|
+
(kind === "activity_status" && ["reading", "searching", "editing"].includes(activityPhase))
|
|
1944
|
+
);
|
|
1940
1945
|
case "commands":
|
|
1941
|
-
return
|
|
1946
|
+
return (
|
|
1947
|
+
kind === "command_event" ||
|
|
1948
|
+
(kind === "file_event" && fileEventType === "git") ||
|
|
1949
|
+
(kind === "activity_status" && activityPhase === "running_command")
|
|
1950
|
+
);
|
|
1942
1951
|
case "approvals":
|
|
1943
|
-
return kind === "approval";
|
|
1952
|
+
return kind === "approval" || (kind === "activity_status" && activityPhase === "awaiting_approval");
|
|
1944
1953
|
case "plans":
|
|
1945
1954
|
return kind === "plan" || kind === "plan_ready";
|
|
1946
1955
|
case "choices":
|
|
@@ -3773,6 +3782,7 @@ function renderTimelineEntry(entry, { desktop }) {
|
|
|
3773
3782
|
const isMessageLike = TIMELINE_MESSAGE_KINDS.has(item.kind) || isMoltbookOrA2A;
|
|
3774
3783
|
const isFileEvent = item.kind === "file_event";
|
|
3775
3784
|
const isCommandEvent = item.kind === "command_event";
|
|
3785
|
+
const isActivityStatus = item.kind === "activity_status";
|
|
3776
3786
|
const imageUrls = Array.isArray(item.imageUrls) ? item.imageUrls.filter(Boolean) : [];
|
|
3777
3787
|
const fileRefs = normalizeClientFileRefs(item.fileRefs);
|
|
3778
3788
|
const primaryText = timelineEntryPrimaryText(item, entry.status, { isMessageLike, isFileEvent, isCommandEvent });
|
|
@@ -3783,13 +3793,15 @@ function renderTimelineEntry(entry, { desktop }) {
|
|
|
3783
3793
|
const fileEventFileSummary = isFileEvent ? timelineFileEventFileSummary(item) : "";
|
|
3784
3794
|
const fileEventDiffStatsHtml = isFileEvent ? renderDiffEntryStatsHtml(item) : "";
|
|
3785
3795
|
const toolEventCommand = timelineToolEventCommand(item);
|
|
3796
|
+
const tagName = isActivityStatus ? "div" : "button";
|
|
3797
|
+
const openAttrs = isActivityStatus
|
|
3798
|
+
? `role="status" aria-live="polite"`
|
|
3799
|
+
: `data-open-item-kind="${escapeHtml(item.kind)}" data-open-item-token="${escapeHtml(item.token)}" data-source-tab="timeline"`;
|
|
3786
3800
|
|
|
3787
3801
|
return `
|
|
3788
|
-
|
|
3789
|
-
class="timeline-entry timeline-entry--${kindClassName} timeline-entry--kind-${kindNameClass} ${isMessageLike ? "timeline-entry--message" : "timeline-entry--operational"}"
|
|
3790
|
-
|
|
3791
|
-
data-open-item-token="${escapeHtml(item.token)}"
|
|
3792
|
-
data-source-tab="timeline"
|
|
3802
|
+
<${tagName}
|
|
3803
|
+
class="timeline-entry timeline-entry--${kindClassName} timeline-entry--kind-${kindNameClass} ${isMessageLike ? "timeline-entry--message" : "timeline-entry--operational"} ${isActivityStatus ? "timeline-entry--activity" : ""}"
|
|
3804
|
+
${openAttrs}
|
|
3793
3805
|
>
|
|
3794
3806
|
<div class="timeline-entry__meta">
|
|
3795
3807
|
<span class="timeline-entry__kind">
|
|
@@ -3799,7 +3811,7 @@ function renderTimelineEntry(entry, { desktop }) {
|
|
|
3799
3811
|
</span>
|
|
3800
3812
|
<span class="timeline-entry__meta-right">
|
|
3801
3813
|
<span class="timeline-entry__time">${escapeHtml(timestampLabel)}</span>
|
|
3802
|
-
|
|
3814
|
+
${isActivityStatus ? `<span class="timeline-entry__activity-pulse" aria-hidden="true"></span>` : `<span class="timeline-entry__chevron" aria-hidden="true">${renderIcon("chevron-right")}</span>`}
|
|
3803
3815
|
</span>
|
|
3804
3816
|
</div>
|
|
3805
3817
|
${threadLabel ? `<p class="timeline-entry__thread">${escapeHtml(threadLabel)}</p>` : ""}
|
|
@@ -3821,7 +3833,7 @@ function renderTimelineEntry(entry, { desktop }) {
|
|
|
3821
3833
|
${isFileEvent ? "" : renderTimelineEntryFileStrip(fileRefs)}
|
|
3822
3834
|
</div>
|
|
3823
3835
|
${statusLabel ? `<div class="timeline-entry__footer"><span class="timeline-entry__status">${escapeHtml(statusLabel)}</span></div>` : ""}
|
|
3824
|
-
|
|
3836
|
+
</${tagName}>
|
|
3825
3837
|
`;
|
|
3826
3838
|
}
|
|
3827
3839
|
|
|
@@ -4059,10 +4071,13 @@ function shouldRenderFileEventCommand(item) {
|
|
|
4059
4071
|
if (item?.kind !== "file_event") {
|
|
4060
4072
|
return false;
|
|
4061
4073
|
}
|
|
4062
|
-
return ["read", "search"].includes(normalizeClientText(item?.fileEventType || ""));
|
|
4074
|
+
return ["read", "search", "git"].includes(normalizeClientText(item?.fileEventType || ""));
|
|
4063
4075
|
}
|
|
4064
4076
|
|
|
4065
4077
|
function timelineToolEventCommand(item) {
|
|
4078
|
+
if (item?.kind === "activity_status") {
|
|
4079
|
+
return truncateUiText(item?.commandText || "", 220);
|
|
4080
|
+
}
|
|
4066
4081
|
if (item?.kind === "command_event") {
|
|
4067
4082
|
return timelineCommandEventCommand(item);
|
|
4068
4083
|
}
|
|
@@ -4078,6 +4093,8 @@ function fileEventDisplayLabel(fileEventType) {
|
|
|
4078
4093
|
return L("fileEvent.read");
|
|
4079
4094
|
case "search":
|
|
4080
4095
|
return L("fileEvent.search");
|
|
4096
|
+
case "git":
|
|
4097
|
+
return L("fileEvent.command");
|
|
4081
4098
|
case "command":
|
|
4082
4099
|
return L("fileEvent.command");
|
|
4083
4100
|
case "write":
|
|
@@ -4095,15 +4112,24 @@ function fileEventDisplayLabel(fileEventType) {
|
|
|
4095
4112
|
|
|
4096
4113
|
function fileEventTimelineCountLabel(item) {
|
|
4097
4114
|
const fileEventType = normalizeClientText(item?.fileEventType || "");
|
|
4098
|
-
const
|
|
4115
|
+
const fileRefs = normalizeClientFileRefs(item?.fileRefs);
|
|
4116
|
+
const count = fileRefs.length;
|
|
4099
4117
|
if (count <= 0) {
|
|
4100
4118
|
return fileEventDisplayLabel(fileEventType) || L("common.fileEvent");
|
|
4101
4119
|
}
|
|
4102
4120
|
switch (fileEventType) {
|
|
4103
4121
|
case "read":
|
|
4122
|
+
if (count === 1) {
|
|
4123
|
+
return L("fileEvent.read");
|
|
4124
|
+
}
|
|
4104
4125
|
return L("fileEvent.timeline.read", { count });
|
|
4105
4126
|
case "search":
|
|
4127
|
+
if (count === 1) {
|
|
4128
|
+
return L("fileEvent.search");
|
|
4129
|
+
}
|
|
4106
4130
|
return L("fileEvent.timeline.search", { count });
|
|
4131
|
+
case "git":
|
|
4132
|
+
return L("fileEvent.timeline.command", { count });
|
|
4107
4133
|
case "command":
|
|
4108
4134
|
return L("fileEvent.timeline.command", { count });
|
|
4109
4135
|
case "write":
|
|
@@ -5448,20 +5474,29 @@ function recentAutoPilotEntries(limit = 5) {
|
|
|
5448
5474
|
}
|
|
5449
5475
|
|
|
5450
5476
|
function isAutoPilotApprovalEntry(entry) {
|
|
5477
|
+
const mode = normalizeClientText(entry?.autoPilotMode || "");
|
|
5451
5478
|
const stableId = normalizeClientText(entry?.stableId || "");
|
|
5452
5479
|
return (
|
|
5453
5480
|
normalizeClientText(entry?.kind || "") === "approval" &&
|
|
5454
5481
|
normalizeClientText(entry?.outcome || "") === "approved" &&
|
|
5455
|
-
(stableId.endsWith(":autopilot") || stableId.includes(":autopilot-write"))
|
|
5482
|
+
(mode === "read" || mode === "write" || stableId.endsWith(":autopilot") || stableId.includes(":autopilot-write"))
|
|
5456
5483
|
);
|
|
5457
5484
|
}
|
|
5458
5485
|
|
|
5459
5486
|
function autoPilotEntryMode(item) {
|
|
5487
|
+
const mode = normalizeClientText(item?.autoPilotMode || "");
|
|
5488
|
+
if (mode === "read" || mode === "write") {
|
|
5489
|
+
return mode;
|
|
5490
|
+
}
|
|
5460
5491
|
const stableId = normalizeClientText(item?.stableId || "");
|
|
5461
5492
|
return stableId.includes(":autopilot-write") ? "write" : "read";
|
|
5462
5493
|
}
|
|
5463
5494
|
|
|
5464
5495
|
function autoPilotEntryWriteLane(item) {
|
|
5496
|
+
const projected = normalizeClientText(item?.autoPilotWriteLane || "");
|
|
5497
|
+
if (projected) {
|
|
5498
|
+
return projected;
|
|
5499
|
+
}
|
|
5465
5500
|
const stableId = normalizeClientText(item?.stableId || "");
|
|
5466
5501
|
const match = stableId.match(/:autopilot-write:([a-z_-]+)$/u);
|
|
5467
5502
|
return normalizeClientText(match?.[1] || "");
|
|
@@ -7627,7 +7662,7 @@ function renderMoltbookDraftComposer(detail, options = {}) {
|
|
|
7627
7662
|
? `
|
|
7628
7663
|
<div class="actions actions--stack${options.mobile ? " actions--sticky" : ""}">
|
|
7629
7664
|
<button type="submit" data-action="approve" class="primary primary--wide">${escapeHtml(approveLabel)}</button>
|
|
7630
|
-
<button type="submit" data-action="deny" class="danger danger--wide"
|
|
7665
|
+
<button type="submit" data-action="deny" class="danger danger--wide">${escapeHtml(L("moltbook.draft.deny"))}</button>
|
|
7631
7666
|
</div>
|
|
7632
7667
|
`
|
|
7633
7668
|
: `<p class="muted reply-composer__description">${escapeHtml(L("moltbook.draft.resolved"))}</p>`;
|
|
@@ -8800,7 +8835,7 @@ function bindShellInteractions() {
|
|
|
8800
8835
|
buildActionOutcomeDetail({
|
|
8801
8836
|
kind: "approval",
|
|
8802
8837
|
title: state.currentDetail?.title,
|
|
8803
|
-
message: approvalOutcomeMessage(actionUrl, activeItem?.provider),
|
|
8838
|
+
message: approvalOutcomeMessage(actionUrl, state.currentDetail?.provider || activeItem?.provider),
|
|
8804
8839
|
})
|
|
8805
8840
|
);
|
|
8806
8841
|
await renderShell();
|
|
@@ -9712,7 +9747,9 @@ for (const button of document.querySelectorAll("[data-wallet-address-copy]")) {
|
|
|
9712
9747
|
labelCache.set(btn, btn.innerHTML);
|
|
9713
9748
|
btn.disabled = true;
|
|
9714
9749
|
if (btn.dataset.action === submittedAction) {
|
|
9715
|
-
btn.innerHTML = submittedAction === "approve"
|
|
9750
|
+
btn.innerHTML = submittedAction === "approve"
|
|
9751
|
+
? escapeHtml(L("moltbook.draft.submittingApprove"))
|
|
9752
|
+
: escapeHtml(L("moltbook.draft.submittingDeny"));
|
|
9716
9753
|
btn.classList.add("is-loading");
|
|
9717
9754
|
} else {
|
|
9718
9755
|
btn.classList.add("is-dimmed");
|
|
@@ -10582,6 +10619,8 @@ function kindMeta(kind, item) {
|
|
|
10582
10619
|
return { label: L("common.assistantCommentary"), tone: "plan", icon: "assistant-commentary" };
|
|
10583
10620
|
case "assistant_final":
|
|
10584
10621
|
return { label: L("common.assistantFinal"), tone: "completion", icon: "assistant-final" };
|
|
10622
|
+
case "activity_status":
|
|
10623
|
+
return { label: L("server.title.activityStatus"), tone: "plan", icon: activityStatusIcon(item?.activityPhase) };
|
|
10585
10624
|
case "ambient_suggestions":
|
|
10586
10625
|
return { label: L("common.ambientSuggestions"), tone: "neutral", icon: "suggestions" };
|
|
10587
10626
|
case "approval":
|
|
@@ -10596,6 +10635,9 @@ function kindMeta(kind, item) {
|
|
|
10596
10635
|
case "diff_thread":
|
|
10597
10636
|
return { label: L("common.diff"), tone: "neutral", icon: "diff" };
|
|
10598
10637
|
case "file_event":
|
|
10638
|
+
if (normalizeClientText(item?.fileEventType || "") === "git") {
|
|
10639
|
+
return { label: L("common.commandEvent"), tone: "neutral", icon: "command" };
|
|
10640
|
+
}
|
|
10599
10641
|
return { label: L("common.fileEvent"), tone: "neutral", icon: "file-event" };
|
|
10600
10642
|
case "command_event":
|
|
10601
10643
|
return { label: L("common.commandEvent"), tone: "neutral", icon: "command" };
|
|
@@ -10616,6 +10658,24 @@ function kindMeta(kind, item) {
|
|
|
10616
10658
|
}
|
|
10617
10659
|
}
|
|
10618
10660
|
|
|
10661
|
+
function activityStatusIcon(phase) {
|
|
10662
|
+
switch (normalizeClientText(phase || "")) {
|
|
10663
|
+
case "reading":
|
|
10664
|
+
return "file-event";
|
|
10665
|
+
case "searching":
|
|
10666
|
+
return "filter";
|
|
10667
|
+
case "editing":
|
|
10668
|
+
return "diff";
|
|
10669
|
+
case "running_command":
|
|
10670
|
+
return "command";
|
|
10671
|
+
case "awaiting_approval":
|
|
10672
|
+
return "approval";
|
|
10673
|
+
case "thinking":
|
|
10674
|
+
default:
|
|
10675
|
+
return "pending";
|
|
10676
|
+
}
|
|
10677
|
+
}
|
|
10678
|
+
|
|
10619
10679
|
function renderTypePillContent(kindInfo) {
|
|
10620
10680
|
return `
|
|
10621
10681
|
<span class="type-pill__icon" aria-hidden="true">${renderIcon(kindInfo.icon)}</span>
|
package/web/build-id.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const APP_BUILD_ID = "
|
|
1
|
+
export const APP_BUILD_ID = "20260509-remote-lan-relay-guard";
|
package/web/i18n.js
CHANGED
|
@@ -463,7 +463,7 @@ const translations = {
|
|
|
463
463
|
"settings.row.defaultLanguage": "Setup default",
|
|
464
464
|
"settings.moltbook.title": "Moltbook",
|
|
465
465
|
"settings.moltbook.subtitle": "Auto-scout & notifications",
|
|
466
|
-
"settings.moltbook.copy": "View
|
|
466
|
+
"settings.moltbook.copy": "View incoming reply drafts, feed scouting, and today's posting quota.",
|
|
467
467
|
"settings.moltbook.unavailable": "Moltbook is not configured.",
|
|
468
468
|
"settings.row.moltbookAccount": "Profile",
|
|
469
469
|
"settings.row.moltbookQuota": "Replies today",
|
|
@@ -484,6 +484,9 @@ const translations = {
|
|
|
484
484
|
"moltbook.draft.titleLabel": "Title",
|
|
485
485
|
"moltbook.draft.approvePost": "Approve & publish",
|
|
486
486
|
"moltbook.draft.approveReply": "Approve & post",
|
|
487
|
+
"moltbook.draft.deny": "Deny",
|
|
488
|
+
"moltbook.draft.submittingApprove": "Publishing...",
|
|
489
|
+
"moltbook.draft.submittingDeny": "Denying...",
|
|
487
490
|
"moltbook.draft.editHint": "Edit if needed, then approve to publish or deny to skip.",
|
|
488
491
|
"moltbook.draft.resolved": "This draft has already been resolved.",
|
|
489
492
|
"moltbook.draft.greetMorning": "Good morning! Share yesterday's highlights?",
|
|
@@ -780,6 +783,7 @@ const translations = {
|
|
|
780
783
|
"server.title.userMessage": "User",
|
|
781
784
|
"server.title.assistantCommentary": "Update",
|
|
782
785
|
"server.title.assistantFinal": "Final reply",
|
|
786
|
+
"server.title.activityStatus": "Working",
|
|
783
787
|
"server.title.ambientSuggestions": "Suggested next steps",
|
|
784
788
|
"server.title.threadShare": "Thread Share",
|
|
785
789
|
"server.action.review": "Review",
|
|
@@ -814,6 +818,13 @@ const translations = {
|
|
|
814
818
|
"server.message.choiceSummarySubmitted": "Selected answers sent.",
|
|
815
819
|
"server.message.choiceSummaryReceivedTest": "Test answers received.",
|
|
816
820
|
"server.message.questionAnswered": "Your answer was sent to {provider}.",
|
|
821
|
+
"server.activity.thinking": "Thinking...",
|
|
822
|
+
"server.activity.reading": "Reading files...",
|
|
823
|
+
"server.activity.searching": "Searching...",
|
|
824
|
+
"server.activity.editing": "Editing...",
|
|
825
|
+
"server.activity.runningCommand": "Running command...",
|
|
826
|
+
"server.activity.awaitingApproval": "Waiting for approval...",
|
|
827
|
+
"server.activity.working": "Working...",
|
|
817
828
|
"claudePlan.title": "Plan from {provider}",
|
|
818
829
|
"claudePlan.approve": "Approve plan",
|
|
819
830
|
"claudePlan.reject": "Reject plan",
|
|
@@ -1529,7 +1540,7 @@ const translations = {
|
|
|
1529
1540
|
"settings.row.defaultLanguage": "setup 時の既定値",
|
|
1530
1541
|
"settings.moltbook.title": "Moltbook",
|
|
1531
1542
|
"settings.moltbook.subtitle": "自動スカウト・通知連携",
|
|
1532
|
-
"settings.moltbook.copy": "
|
|
1543
|
+
"settings.moltbook.copy": "自分宛コメントの返信案、フィード巡回、本日の投稿上限を確認できます。",
|
|
1533
1544
|
"settings.moltbook.unavailable": "Moltbook が設定されていません。",
|
|
1534
1545
|
"settings.row.moltbookAccount": "プロフィール",
|
|
1535
1546
|
"settings.row.moltbookQuota": "本日の返信数",
|
|
@@ -1550,6 +1561,9 @@ const translations = {
|
|
|
1550
1561
|
"moltbook.draft.titleLabel": "タイトル",
|
|
1551
1562
|
"moltbook.draft.approvePost": "承認して投稿",
|
|
1552
1563
|
"moltbook.draft.approveReply": "承認して返信",
|
|
1564
|
+
"moltbook.draft.deny": "拒否",
|
|
1565
|
+
"moltbook.draft.submittingApprove": "投稿中…",
|
|
1566
|
+
"moltbook.draft.submittingDeny": "拒否中…",
|
|
1553
1567
|
"moltbook.draft.editHint": "必要に応じて編集し、承認して投稿するか、拒否してスキップします。",
|
|
1554
1568
|
"moltbook.draft.resolved": "このドラフトは既に処理済みです。",
|
|
1555
1569
|
"moltbook.draft.greetMorning": "おはようございます!昨日の成果をシェアしませんか?",
|
|
@@ -1846,6 +1860,7 @@ const translations = {
|
|
|
1846
1860
|
"server.title.userMessage": "ユーザー",
|
|
1847
1861
|
"server.title.assistantCommentary": "途中経過",
|
|
1848
1862
|
"server.title.assistantFinal": "最終回答",
|
|
1863
|
+
"server.title.activityStatus": "作業中",
|
|
1849
1864
|
"server.title.ambientSuggestions": "次の候補",
|
|
1850
1865
|
"server.title.threadShare": "スレッド共有",
|
|
1851
1866
|
"server.action.review": "判断する",
|
|
@@ -1880,6 +1895,13 @@ const translations = {
|
|
|
1880
1895
|
"server.message.choiceSummarySubmitted": "選択内容を送信しました。",
|
|
1881
1896
|
"server.message.choiceSummaryReceivedTest": "テスト回答を受信しました。",
|
|
1882
1897
|
"server.message.questionAnswered": "回答を {provider} に送信しました。",
|
|
1898
|
+
"server.activity.thinking": "考え中...",
|
|
1899
|
+
"server.activity.reading": "ファイルを読んでいます...",
|
|
1900
|
+
"server.activity.searching": "検索しています...",
|
|
1901
|
+
"server.activity.editing": "編集中...",
|
|
1902
|
+
"server.activity.runningCommand": "コマンドを実行中...",
|
|
1903
|
+
"server.activity.awaitingApproval": "承認待ち...",
|
|
1904
|
+
"server.activity.working": "作業中...",
|
|
1883
1905
|
"claudePlan.title": "{provider} からのプラン",
|
|
1884
1906
|
"claudePlan.approve": "プランを承認",
|
|
1885
1907
|
"claudePlan.reject": "プランを却下",
|
|
@@ -13,12 +13,13 @@
|
|
|
13
13
|
* 2. If LAN throws a `TypeError` (network failure, DNS, connect refused —
|
|
14
14
|
* i.e. the bridge isn't reachable), fall back to the relay's
|
|
15
15
|
* `RemotePairingRpcClient.fetch()`.
|
|
16
|
-
* 3. After a LAN failure, enter a
|
|
17
|
-
* requests skip LAN and go straight to relay.
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
16
|
+
* 3. After a LAN failure, enter a sticky relay window where subsequent
|
|
17
|
+
* requests skip LAN and go straight to relay. If LAN was healthy very
|
|
18
|
+
* recently, keep that window short so one transient WiFi wobble doesn't
|
|
19
|
+
* make an in-room phone behave like it is off-LAN for minutes.
|
|
20
|
+
* 4. While sticky relay is active, periodically run a tiny LAN probe. If
|
|
21
|
+
* LAN is back, clear sticky state and close the relay client. If LAN is
|
|
22
|
+
* still dead, the sticky window resets and relay stays the fast path.
|
|
22
23
|
* 5. AbortError (caller cancelled) is never treated as a LAN failure —
|
|
23
24
|
* we re-throw immediately rather than waste a relay attempt.
|
|
24
25
|
*
|
|
@@ -76,11 +77,9 @@ import { loadPairingState } from "./pairing-state.js";
|
|
|
76
77
|
// ---------------------------------------------------------------------------
|
|
77
78
|
|
|
78
79
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* outbound call, short enough that "I just walked back into wifi range"
|
|
83
|
-
* recovers without the user noticing.
|
|
80
|
+
* Hard upper bound after a LAN failure. We keep this for real off-LAN use so
|
|
81
|
+
* cellular/remote sessions do not pay LAN connect timeouts on every request.
|
|
82
|
+
* Recent successful LAN sessions use a much shorter transient window below.
|
|
84
83
|
*/
|
|
85
84
|
const STICKY_RELAY_MS = 5 * 60 * 1000;
|
|
86
85
|
|
|
@@ -99,6 +98,20 @@ const DEFAULT_RELAY_TIMEOUT_MS = 60_000;
|
|
|
99
98
|
*/
|
|
100
99
|
const DEFAULT_LAN_TIMEOUT_MS = 2_500;
|
|
101
100
|
const DEFAULT_STICKY_LAN_PROBE_TIMEOUT_MS = 350;
|
|
101
|
+
// Recover quickly from accidentally sticky relay while still avoiding LAN
|
|
102
|
+
// timeout spam during real off-LAN sessions.
|
|
103
|
+
const AUTO_STICKY_LAN_PROBE_INTERVAL_MS = 8_000;
|
|
104
|
+
const AUTO_STICKY_LAN_PROBE_TIMEOUT_MS = 900;
|
|
105
|
+
const LAN_REACHABILITY_PROBE_TIMEOUT_MS = 700;
|
|
106
|
+
const LAN_REACHABILITY_PROBE_PATH = "/health";
|
|
107
|
+
const RECENT_LAN_OK_GRACE_MS = 60_000;
|
|
108
|
+
const TRANSIENT_LAN_FAILURE_STICKY_MS = 8_000;
|
|
109
|
+
const TRANSIENT_LAN_FAILURE_THRESHOLD = 2;
|
|
110
|
+
// If LAN was healthy moments ago, a single timed-out local request is more
|
|
111
|
+
// likely a busy bridge/Safari wobble than a true off-LAN transition. Delay
|
|
112
|
+
// relay fallback for the first few consecutive failures to avoid burning
|
|
113
|
+
// Cloudflare Durable Object quota while the phone is still on trusted LAN.
|
|
114
|
+
const RECENT_LAN_RELAY_SUPPRESSION_FAILURES = 2;
|
|
102
115
|
const RELAY_FAILURE_WINDOW_MS = 60_000;
|
|
103
116
|
const RELAY_FAILURE_THRESHOLD = 6;
|
|
104
117
|
const RELAY_CIRCUIT_BREAKER_MS = 60_000;
|
|
@@ -132,6 +145,15 @@ let _lastPairingStateStatus = null;
|
|
|
132
145
|
/** Last transport route that completed successfully. */
|
|
133
146
|
let _lastSuccessfulRoute = null;
|
|
134
147
|
|
|
148
|
+
/** Last time a LAN request completed successfully. */
|
|
149
|
+
let _lastLanOkAtMs = 0;
|
|
150
|
+
|
|
151
|
+
/** Last time we tried a LAN probe while sticky relay was active. */
|
|
152
|
+
let _lastStickyLanProbeAtMs = 0;
|
|
153
|
+
|
|
154
|
+
/** Consecutive LAN transport failures since the last LAN success. */
|
|
155
|
+
let _consecutiveLanFailures = 0;
|
|
156
|
+
|
|
135
157
|
/** Recent relay-level failures used to avoid burning relay/DO quota in loops. */
|
|
136
158
|
let _relayFailureAtMs = [];
|
|
137
159
|
|
|
@@ -142,8 +164,10 @@ function newTelemetry() {
|
|
|
142
164
|
return {
|
|
143
165
|
lanOk: 0,
|
|
144
166
|
lanFail: 0,
|
|
167
|
+
lanReachableAfterFailure: 0,
|
|
145
168
|
relayOk: 0,
|
|
146
169
|
relayFail: 0,
|
|
170
|
+
relaySuppressed: 0,
|
|
147
171
|
lastLanFailAt: 0,
|
|
148
172
|
lastRelayFailAt: 0,
|
|
149
173
|
clientResets: 0,
|
|
@@ -440,6 +464,29 @@ function relayCircuitError(opts = {}) {
|
|
|
440
464
|
return err;
|
|
441
465
|
}
|
|
442
466
|
|
|
467
|
+
function shouldAutoProbeLanWhileSticky(now, opts = {}) {
|
|
468
|
+
if (opts.autoProbeLanWhileSticky === false) {
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
if (relayCircuitDelayMs(opts) > 0) {
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
return now - _lastStickyLanProbeAtMs >= AUTO_STICKY_LAN_PROBE_INTERVAL_MS;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function stickyLanProbeOpts(opts, autoProbe) {
|
|
478
|
+
if (!autoProbe) {
|
|
479
|
+
return opts;
|
|
480
|
+
}
|
|
481
|
+
if (opts.stickyLanProbeTimeoutMs != null) {
|
|
482
|
+
return opts;
|
|
483
|
+
}
|
|
484
|
+
return {
|
|
485
|
+
...opts,
|
|
486
|
+
stickyLanProbeTimeoutMs: AUTO_STICKY_LAN_PROBE_TIMEOUT_MS,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
443
490
|
function recordRelayFailure(err, opts = {}) {
|
|
444
491
|
const now = nowMs(opts);
|
|
445
492
|
_relayFailureAtMs = _relayFailureAtMs.filter((at) => now - at <= RELAY_FAILURE_WINDOW_MS);
|
|
@@ -554,8 +601,11 @@ async function attemptLanFetch(url, init, opts) {
|
|
|
554
601
|
? await Promise.race([fetchPromise, timeoutPromise])
|
|
555
602
|
: await fetchPromise;
|
|
556
603
|
_telemetry.lanOk++;
|
|
604
|
+
_lastLanOkAtMs = nowMs(opts);
|
|
605
|
+
_consecutiveLanFailures = 0;
|
|
557
606
|
_lastSuccessfulRoute = "lan";
|
|
558
607
|
_stickyRelayUntilMs = 0;
|
|
608
|
+
_lastStickyLanProbeAtMs = 0;
|
|
559
609
|
resetRelayFailureCircuit();
|
|
560
610
|
closeRelayClient("lan connected");
|
|
561
611
|
emitRoutingStatus("lan-connected", opts, { url: String(url || "") });
|
|
@@ -566,12 +616,17 @@ async function attemptLanFetch(url, init, opts) {
|
|
|
566
616
|
throw err;
|
|
567
617
|
}
|
|
568
618
|
const lanErr = didTimeout ? new TypeError("LAN fetch timed out") : err;
|
|
619
|
+
const failedAt = nowMs(opts);
|
|
569
620
|
_telemetry.lanFail++;
|
|
570
|
-
|
|
571
|
-
|
|
621
|
+
_consecutiveLanFailures++;
|
|
622
|
+
_telemetry.lastLanFailAt = failedAt;
|
|
623
|
+
_lastStickyLanProbeAtMs = failedAt;
|
|
624
|
+
_stickyRelayUntilMs = failedAt + stickyRelayWindowAfterLanFailure(failedAt);
|
|
572
625
|
emitRoutingStatus("lan-failed", opts, {
|
|
573
626
|
url: String(url || ""),
|
|
574
627
|
reason: lanErr?.message || String(lanErr),
|
|
628
|
+
consecutiveLanFailures: _consecutiveLanFailures,
|
|
629
|
+
stickyRelayMs: Math.max(0, _stickyRelayUntilMs - failedAt),
|
|
575
630
|
});
|
|
576
631
|
return { ok: false, err: lanErr };
|
|
577
632
|
} finally {
|
|
@@ -582,8 +637,71 @@ async function attemptLanFetch(url, init, opts) {
|
|
|
582
637
|
}
|
|
583
638
|
}
|
|
584
639
|
|
|
640
|
+
function stickyRelayWindowAfterLanFailure(now) {
|
|
641
|
+
const recentLanOk = _lastLanOkAtMs > 0 && now - _lastLanOkAtMs <= RECENT_LAN_OK_GRACE_MS;
|
|
642
|
+
if (recentLanOk && _consecutiveLanFailures <= TRANSIENT_LAN_FAILURE_THRESHOLD) {
|
|
643
|
+
return TRANSIENT_LAN_FAILURE_STICKY_MS;
|
|
644
|
+
}
|
|
645
|
+
return STICKY_RELAY_MS;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function shouldDelayRelayAfterRecentLanFailure(now, opts = {}) {
|
|
649
|
+
if (opts.delayRelayAfterRecentLanFailure === false) {
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
if (_lastLanOkAtMs <= 0 || now - _lastLanOkAtMs > RECENT_LAN_OK_GRACE_MS) {
|
|
653
|
+
return false;
|
|
654
|
+
}
|
|
655
|
+
return _consecutiveLanFailures > 0 &&
|
|
656
|
+
_consecutiveLanFailures <= RECENT_LAN_RELAY_SUPPRESSION_FAILURES;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function suppressRelayAfterRecentLanFailure(url, err, opts = {}) {
|
|
660
|
+
_telemetry.relaySuppressed++;
|
|
661
|
+
emitRoutingStatus("remote-delayed", opts, {
|
|
662
|
+
url: String(url || ""),
|
|
663
|
+
reason: err?.message || String(err || "LAN failure"),
|
|
664
|
+
consecutiveLanFailures: _consecutiveLanFailures,
|
|
665
|
+
recentLanOkAtMs: _lastLanOkAtMs,
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function shouldConfirmLanReachability(now, opts = {}) {
|
|
670
|
+
if (opts.confirmLanReachabilityBeforeRelay === false) {
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
return _lastLanOkAtMs > 0 && now - _lastLanOkAtMs <= RECENT_LAN_OK_GRACE_MS;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
async function confirmLanUnavailableBeforeRelay(url, err, opts = {}) {
|
|
677
|
+
if (!shouldConfirmLanReachability(nowMs(opts), opts)) {
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
const path = urlToRelayPath(url);
|
|
681
|
+
if (path === LAN_REACHABILITY_PROBE_PATH) {
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const probe = await attemptLanFetch(LAN_REACHABILITY_PROBE_PATH, {
|
|
686
|
+
cache: "no-store",
|
|
687
|
+
credentials: "same-origin",
|
|
688
|
+
headers: { Accept: "application/json" },
|
|
689
|
+
}, {
|
|
690
|
+
...opts,
|
|
691
|
+
lanTimeoutMs: opts.lanReachabilityProbeTimeoutMs ?? LAN_REACHABILITY_PROBE_TIMEOUT_MS,
|
|
692
|
+
});
|
|
693
|
+
if (!probe.ok) {
|
|
694
|
+
return false;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
_telemetry.lanReachableAfterFailure++;
|
|
698
|
+
suppressRelayAfterRecentLanFailure(url, err, opts);
|
|
699
|
+
return true;
|
|
700
|
+
}
|
|
701
|
+
|
|
585
702
|
async function attemptStickyLanProbe(url, init, opts) {
|
|
586
703
|
const probeTimeoutMs = opts.stickyLanProbeTimeoutMs ?? DEFAULT_STICKY_LAN_PROBE_TIMEOUT_MS;
|
|
704
|
+
_lastStickyLanProbeAtMs = nowMs(opts);
|
|
587
705
|
const lan = await attemptLanFetch(url, init, {
|
|
588
706
|
...opts,
|
|
589
707
|
lanTimeoutMs: probeTimeoutMs,
|
|
@@ -859,7 +977,11 @@ function nowMs(opts) {
|
|
|
859
977
|
* suppressRoutingStatus?: boolean,
|
|
860
978
|
* preferRelayError?: boolean,
|
|
861
979
|
* probeLanWhileSticky?: boolean,
|
|
980
|
+
* autoProbeLanWhileSticky?: boolean,
|
|
981
|
+
* delayRelayAfterRecentLanFailure?: boolean,
|
|
982
|
+
* confirmLanReachabilityBeforeRelay?: boolean,
|
|
862
983
|
* stickyLanProbeTimeoutMs?: number,
|
|
984
|
+
* lanReachabilityProbeTimeoutMs?: number,
|
|
863
985
|
* }} [opts]
|
|
864
986
|
* @returns {Promise<{
|
|
865
987
|
* ok: boolean,
|
|
@@ -875,9 +997,24 @@ export async function routedFetch(url, init = {}, opts = {}) {
|
|
|
875
997
|
|
|
876
998
|
// Sticky-relay path: LAN just failed, prefer relay for a while.
|
|
877
999
|
if (_stickyRelayUntilMs > t) {
|
|
878
|
-
if (opts
|
|
879
|
-
const lan = await
|
|
1000
|
+
if (shouldDelayRelayAfterRecentLanFailure(t, opts)) {
|
|
1001
|
+
const lan = await attemptLanFetch(url, init, opts);
|
|
1002
|
+
if (lan.ok) return lan.response;
|
|
1003
|
+
if (shouldDelayRelayAfterRecentLanFailure(nowMs(opts), opts)) {
|
|
1004
|
+
suppressRelayAfterRecentLanFailure(url, lan.err, opts);
|
|
1005
|
+
throw lan.err;
|
|
1006
|
+
}
|
|
1007
|
+
if (await confirmLanUnavailableBeforeRelay(url, lan.err, opts)) {
|
|
1008
|
+
throw lan.err;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
const autoProbe = shouldAutoProbeLanWhileSticky(t, opts);
|
|
1012
|
+
if (opts.probeLanWhileSticky === true || autoProbe) {
|
|
1013
|
+
const lan = await attemptStickyLanProbe(url, init, stickyLanProbeOpts(opts, autoProbe));
|
|
880
1014
|
if (lan.ok) return lan.response;
|
|
1015
|
+
if (await confirmLanUnavailableBeforeRelay(url, lan.err, opts)) {
|
|
1016
|
+
throw lan.err;
|
|
1017
|
+
}
|
|
881
1018
|
}
|
|
882
1019
|
emitRoutingStatus("remote-switching", opts, { url: String(url || ""), sticky: true });
|
|
883
1020
|
const r = await attemptRelayFetch(url, init, opts);
|
|
@@ -897,6 +1034,13 @@ export async function routedFetch(url, init = {}, opts = {}) {
|
|
|
897
1034
|
// Happy path: LAN first.
|
|
898
1035
|
const lan = await attemptLanFetch(url, init, opts);
|
|
899
1036
|
if (lan.ok) return lan.response;
|
|
1037
|
+
if (shouldDelayRelayAfterRecentLanFailure(nowMs(opts), opts)) {
|
|
1038
|
+
suppressRelayAfterRecentLanFailure(url, lan.err, opts);
|
|
1039
|
+
throw lan.err;
|
|
1040
|
+
}
|
|
1041
|
+
if (await confirmLanUnavailableBeforeRelay(url, lan.err, opts)) {
|
|
1042
|
+
throw lan.err;
|
|
1043
|
+
}
|
|
900
1044
|
|
|
901
1045
|
// Try relay once before giving up.
|
|
902
1046
|
emitRoutingStatus("remote-switching", opts, { url: String(url || ""), sticky: false });
|
|
@@ -918,6 +1062,9 @@ export function __getTelemetry() {
|
|
|
918
1062
|
..._telemetry,
|
|
919
1063
|
stickyRelayUntilMs: _stickyRelayUntilMs,
|
|
920
1064
|
lastRoute: _lastSuccessfulRoute,
|
|
1065
|
+
lastLanOkAtMs: _lastLanOkAtMs,
|
|
1066
|
+
lastStickyLanProbeAtMs: _lastStickyLanProbeAtMs,
|
|
1067
|
+
consecutiveLanFailures: _consecutiveLanFailures,
|
|
921
1068
|
hasClient: Boolean(_client),
|
|
922
1069
|
clientPairingKey: _clientPairingKey,
|
|
923
1070
|
relayCircuitOpenUntilMs: _relayCircuitOpenUntilMs,
|
|
@@ -946,6 +1093,9 @@ export function __resetForTest() {
|
|
|
946
1093
|
_telemetry = newTelemetry();
|
|
947
1094
|
_lastPairingStateStatus = null;
|
|
948
1095
|
_lastSuccessfulRoute = null;
|
|
1096
|
+
_lastLanOkAtMs = 0;
|
|
1097
|
+
_lastStickyLanProbeAtMs = 0;
|
|
1098
|
+
_consecutiveLanFailures = 0;
|
|
949
1099
|
_relayFailureAtMs = [];
|
|
950
1100
|
_relayCircuitOpenUntilMs = 0;
|
|
951
1101
|
}
|
|
@@ -953,5 +1103,8 @@ export function __resetForTest() {
|
|
|
953
1103
|
// Test-visible constants for tests that want to assert behavior at the
|
|
954
1104
|
// sticky-window boundary without hard-coding 5 minutes in two places.
|
|
955
1105
|
export const __STICKY_RELAY_MS = STICKY_RELAY_MS;
|
|
1106
|
+
export const __AUTO_STICKY_LAN_PROBE_INTERVAL_MS = AUTO_STICKY_LAN_PROBE_INTERVAL_MS;
|
|
1107
|
+
export const __TRANSIENT_LAN_FAILURE_STICKY_MS = TRANSIENT_LAN_FAILURE_STICKY_MS;
|
|
956
1108
|
export const __DEFAULT_RELAY_TIMEOUT_MS = DEFAULT_RELAY_TIMEOUT_MS;
|
|
957
1109
|
export const __DEFAULT_LAN_TIMEOUT_MS = DEFAULT_LAN_TIMEOUT_MS;
|
|
1110
|
+
export const __RECENT_LAN_RELAY_SUPPRESSION_FAILURES = RECENT_LAN_RELAY_SUPPRESSION_FAILURES;
|