create-walle 0.10.1 → 0.10.2
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 +4 -0
- package/lib/owner-write-safety.js +135 -0
- package/lib/public-runtime.js +13 -5
- package/lib/release-schema.json +3 -2
- package/lib/runtime-closure-policy.json +6 -4
- package/package.json +1 -1
- package/template/bin/deps-doctor.js +1 -1
- package/template/bin/node-bin.sh +11 -0
- package/template/coding-task-manager/api-prompts.js +10 -22
- package/template/coding-task-manager/bin/claude-hook.js +7 -3
- package/template/coding-task-manager/bin/claude-table-parity.js +307 -0
- package/template/coding-task-manager/bin/conversation-storage-auto-migration-lib.js +16 -1
- package/template/coding-task-manager/bin/conversation-storage-generation-evidence.js +9 -2
- package/template/coding-task-manager/bin/conversation-storage-service-entry-lib.js +16 -1
- package/template/coding-task-manager/bin/conversation-storage-walle-owner.js +247 -0
- package/template/coding-task-manager/bin/ctm-machine-pair.js +160 -0
- package/template/coding-task-manager/bin/lead-wait-census.js +1348 -0
- package/template/coding-task-manager/bin/repair-foreign-clone.js +564 -0
- package/template/coding-task-manager/bin/repair-resume-echo-titles.js +248 -0
- package/template/coding-task-manager/bin/write-lock-bench.js +111 -0
- package/template/coding-task-manager/db.js +705 -286
- package/template/coding-task-manager/git-utils.js +44 -0
- package/template/coding-task-manager/lib/adopted-host-redial.js +4 -0
- package/template/coding-task-manager/lib/agent-presets.js +15 -0
- package/template/coding-task-manager/lib/agent-title-apply.js +88 -0
- package/template/coding-task-manager/lib/agent-title-backfill-read.js +92 -0
- package/template/coding-task-manager/lib/api/host-stats.js +98 -0
- package/template/coding-task-manager/lib/api/ingress.js +35 -12
- package/template/coding-task-manager/lib/api/machine-pairing.js +285 -0
- package/template/coding-task-manager/lib/api/machine-pool.js +307 -0
- package/template/coding-task-manager/lib/api/machines.js +461 -0
- package/template/coding-task-manager/lib/api/misc-ops.js +20 -5
- package/template/coding-task-manager/lib/api/providers.js +53 -6
- package/template/coding-task-manager/lib/api/resume-from-store.js +25 -0
- package/template/coding-task-manager/lib/api/sessions.js +160 -8
- package/template/coding-task-manager/lib/api/transcript-history.js +17 -4
- package/template/coding-task-manager/lib/api/worktrees.js +38 -37
- package/template/coding-task-manager/lib/approval-drift.js +16 -1
- package/template/coding-task-manager/lib/approval-logical-frame.js +5 -1
- package/template/coding-task-manager/lib/approval-scope.js +68 -4
- package/template/coding-task-manager/lib/auth-context.js +32 -2
- package/template/coding-task-manager/lib/auth-rate-limit.js +41 -1
- package/template/coding-task-manager/lib/auth-rules.js +186 -45
- package/template/coding-task-manager/lib/broadcast-serialize.js +2 -1
- package/template/coding-task-manager/lib/browser-session-broker.js +9 -1
- package/template/coding-task-manager/lib/browser-use-adapter.js +33 -2
- package/template/coding-task-manager/lib/claude-markdown-ansi.js +27 -85
- package/template/coding-task-manager/lib/claude-session-title.js +6 -30
- package/template/coding-task-manager/lib/claude-table-layout.js +322 -0
- package/template/coding-task-manager/lib/client-diag-handler.js +5 -0
- package/template/coding-task-manager/lib/codex-config-guard.js +33 -3
- package/template/coding-task-manager/lib/codex-harness-qualification.js +12 -0
- package/template/coding-task-manager/lib/codex-launch-health.js +29 -2
- package/template/coding-task-manager/lib/codex-link-preparation.js +9 -8
- package/template/coding-task-manager/lib/codex-paths.js +2 -1
- package/template/coding-task-manager/lib/coding-agent-models.js +21 -30
- package/template/coding-task-manager/lib/coding-session-model-adapters.js +27 -2
- package/template/coding-task-manager/lib/coding-session-model-projection.js +5 -0
- package/template/coding-task-manager/lib/coding-session-model-runtime.js +1 -2
- package/template/coding-task-manager/lib/coding-session-model-state.js +10 -0
- package/template/coding-task-manager/lib/coding-session-model-sync-state.js +0 -1
- package/template/coding-task-manager/lib/conversation-import/cursor-guard-owner.js +10 -0
- package/template/coding-task-manager/lib/create/resolve-launch-params.js +8 -1
- package/template/coding-task-manager/lib/ctm-boot-lifecycle.js +17 -1
- package/template/coding-task-manager/lib/db-domain-tables.js +7 -0
- package/template/coding-task-manager/lib/db-domains.js +21 -2
- package/template/coding-task-manager/lib/db-owner-op-registry.js +7 -0
- package/template/coding-task-manager/lib/db-schema-source-files.js +1 -0
- package/template/coding-task-manager/lib/feature-manifest.js +1 -1
- package/template/coding-task-manager/lib/fs-watcher.js +3 -2
- package/template/coding-task-manager/lib/harness-history-transition.js +3 -3
- package/template/coding-task-manager/lib/headless-inflight-ops.js +203 -0
- package/template/coding-task-manager/lib/headless-missing-reply.js +42 -0
- package/template/coding-task-manager/lib/headless-term-service.js +55 -17
- package/template/coding-task-manager/lib/host-control-dial.js +167 -0
- package/template/coding-task-manager/lib/host-handoff-orchestrator.js +16 -2
- package/template/coding-task-manager/lib/host-handoff.js +51 -13
- package/template/coding-task-manager/lib/host-health-ledger.js +164 -12
- package/template/coding-task-manager/lib/host-self-truth-watchdog.js +4 -0
- package/template/coding-task-manager/lib/host-stats.js +328 -0
- package/template/coding-task-manager/lib/host-threadpool-evacuation.js +160 -0
- package/template/coding-task-manager/lib/host-threadpool-monitor.js +14 -1
- package/template/coding-task-manager/lib/hosted-input-dispatch.js +203 -0
- package/template/coding-task-manager/lib/hosted-sync-worker.js +113 -5
- package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +4 -0
- package/template/coding-task-manager/lib/instance-manifest-runtime.js +3 -0
- package/template/coding-task-manager/lib/instance-manifest.js +13 -1
- package/template/coding-task-manager/lib/instance-sync-status.js +15 -0
- package/template/coding-task-manager/lib/launch-model-override.js +160 -1
- package/template/coding-task-manager/lib/launch-plan-worker.js +59 -2
- package/template/coding-task-manager/lib/lead-model-switch-runtime.js +57 -4
- package/template/coding-task-manager/lib/lead-model-switch.js +3 -2
- package/template/coding-task-manager/lib/lead-model-transition-store.js +61 -3
- package/template/coding-task-manager/lib/legacy-draft-cancellation-recovery.js +111 -0
- package/template/coding-task-manager/lib/login-tripwire.js +1 -0
- package/template/coding-task-manager/lib/machine-pairing.js +238 -0
- package/template/coding-task-manager/lib/machine-pool.js +409 -0
- package/template/coding-task-manager/lib/machine-registry.js +749 -0
- package/template/coding-task-manager/lib/machine-self.js +66 -0
- package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +52 -4
- package/template/coding-task-manager/lib/media-journal-control-reader.js +473 -0
- package/template/coding-task-manager/lib/media-journal.js +84 -27
- package/template/coding-task-manager/lib/media-store-options.js +43 -15
- package/template/coding-task-manager/lib/migrations/v56-host-samples-and-machine-pool.js +76 -0
- package/template/coding-task-manager/lib/mirror-feed-sanitize.js +12 -3
- package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +4 -0
- package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +3 -0
- package/template/coding-task-manager/lib/model-proxy/handoff.js +494 -0
- package/template/coding-task-manager/lib/model-proxy/lead-worker-picks.js +25 -0
- package/template/coding-task-manager/lib/model-proxy/native-lead-listener.js +61 -7
- package/template/coding-task-manager/lib/model-proxy/proxy-cli.js +200 -0
- package/template/coding-task-manager/lib/model-proxy/proxy-instance-record.js +257 -0
- package/template/coding-task-manager/lib/model-proxy/stream-accounting.js +205 -0
- package/template/coding-task-manager/lib/model-proxy/tokens.js +78 -0
- package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +9 -0
- package/template/coding-task-manager/lib/native-history-control.js +2 -2
- package/template/coding-task-manager/lib/native-lead-settings-client.js +2 -1
- package/template/coding-task-manager/lib/native-lead-tls.js +18 -4
- package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +628 -64
- package/template/coding-task-manager/lib/owner-action-manifest-data.json +14 -0
- package/template/coding-task-manager/lib/painted-row-recorder.js +9 -1
- package/template/coding-task-manager/lib/prompt-submission-contract.js +21 -1
- package/template/coding-task-manager/lib/provider-login-broker.js +192 -9
- package/template/coding-task-manager/lib/provider-login-capture.js +153 -0
- package/template/coding-task-manager/lib/queue-draft-status-overlay.js +36 -0
- package/template/coding-task-manager/lib/raw-codex-history-window.js +5 -3
- package/template/coding-task-manager/lib/read-pool-client.js +5 -2
- package/template/coding-task-manager/lib/read-pool-op-registry.js +10 -1
- package/template/coding-task-manager/lib/recent-catalog-query.js +26 -0
- package/template/coding-task-manager/lib/recent-session-catalog-response.js +9 -0
- package/template/coding-task-manager/lib/remote-command-context.js +12 -0
- package/template/coding-task-manager/lib/remote-command-runtime.js +13 -1
- package/template/coding-task-manager/lib/remote-machine-catalog.js +1062 -0
- package/template/coding-task-manager/lib/remote-machine-create.js +693 -0
- package/template/coding-task-manager/lib/remote-machine-link.js +939 -0
- package/template/coding-task-manager/lib/remote-machine-proxy.js +520 -0
- package/template/coding-task-manager/lib/remote-session-forward.js +306 -0
- package/template/coding-task-manager/lib/remote-submission-status.js +6 -0
- package/template/coding-task-manager/lib/restart-outcome-truth.js +20 -3
- package/template/coding-task-manager/lib/restart-preflight.js +9 -2
- package/template/coding-task-manager/lib/restore-backoff-gate.js +10 -2
- package/template/coding-task-manager/lib/restore-prepare-retry.js +12 -5
- package/template/coding-task-manager/lib/restore-resume-batch.js +22 -1
- package/template/coding-task-manager/lib/resume-blocked-wire.js +62 -1
- package/template/coding-task-manager/lib/resume-cwd.js +13 -0
- package/template/coding-task-manager/lib/resume-from-store.js +12 -1
- package/template/coding-task-manager/lib/resume-origin-facts.js +38 -1
- package/template/coding-task-manager/lib/resume-preflight.js +21 -16
- package/template/coding-task-manager/lib/review-repo-admission.js +39 -6
- package/template/coding-task-manager/lib/review-repo-registry.js +17 -0
- package/template/coding-task-manager/lib/root-delivery-runtime.js +72 -15
- package/template/coding-task-manager/lib/root-delivery-selection.js +37 -1
- package/template/coding-task-manager/lib/root-prompt-admission.js +33 -2
- package/template/coding-task-manager/lib/root-prompt-flow.js +167 -33
- package/template/coding-task-manager/lib/scrollback-archive-store.js +7 -4
- package/template/coding-task-manager/lib/session-analysis-memory.js +2 -0
- package/template/coding-task-manager/lib/session-create/codex-native-provider-gate.js +218 -0
- package/template/coding-task-manager/lib/session-create/codex-resume-exit-classify.js +52 -0
- package/template/coding-task-manager/lib/session-create/delegate-worker.js +95 -6
- package/template/coding-task-manager/lib/session-create/identity.js +20 -1
- package/template/coding-task-manager/lib/session-create/prepared.js +69 -1
- package/template/coding-task-manager/lib/session-create/provider-launch-context.js +77 -0
- package/template/coding-task-manager/lib/session-create/provider-process.js +34 -37
- package/template/coding-task-manager/lib/session-create/response.js +30 -12
- package/template/coding-task-manager/lib/session-create/transcript.js +13 -4
- package/template/coding-task-manager/lib/session-create/worker-continuation.js +43 -1
- package/template/coding-task-manager/lib/session-create/worker-notices.js +94 -5
- package/template/coding-task-manager/lib/session-create/worker-observability.js +99 -7
- package/template/coding-task-manager/lib/session-create/worker-stop.js +11 -3
- package/template/coding-task-manager/lib/session-create/worker-wait-watcher.js +315 -0
- package/template/coding-task-manager/lib/session-geometry-authority-handler.js +10 -1
- package/template/coding-task-manager/lib/session-host-manager.js +152 -206
- package/template/coding-task-manager/lib/session-jobs.js +17 -9
- package/template/coding-task-manager/lib/session-lifecycle.js +18 -0
- package/template/coding-task-manager/lib/session-model-action-preconditions.js +9 -0
- package/template/coding-task-manager/lib/session-resource-sampler.js +19 -9
- package/template/coding-task-manager/lib/session-standup.js +67 -4
- package/template/coding-task-manager/lib/session-status-ledger.js +42 -3
- package/template/coding-task-manager/lib/session-status-runtime.js +45 -1
- package/template/coding-task-manager/lib/session-title-mirror.js +25 -57
- package/template/coding-task-manager/lib/session-transcript-trash.js +8 -1
- package/template/coding-task-manager/lib/startup-pointer-write.js +57 -0
- package/template/coding-task-manager/lib/state-sync/painted-frame-field.js +11 -0
- package/template/coding-task-manager/lib/storage/coding-session-model-store.js +21 -22
- package/template/coding-task-manager/lib/surviving-owner-bootstrap.js +7 -2
- package/template/coding-task-manager/lib/surviving-owner-recovery.js +2 -1
- package/template/coding-task-manager/lib/terminal-draft-http-status.js +25 -0
- package/template/coding-task-manager/lib/transcript-archiver.js +229 -229
- package/template/coding-task-manager/lib/transcript-frame-cache.js +12 -2
- package/template/coding-task-manager/lib/transcript-guard-job.js +55 -4
- package/template/coding-task-manager/lib/transcript-link.js +77 -0
- package/template/coding-task-manager/lib/transcript-manifest-store.js +8 -4
- package/template/coding-task-manager/lib/transcript-source.js +8 -3
- package/template/coding-task-manager/lib/transcript-store.js +40 -0
- package/template/coding-task-manager/lib/tunnel-lane-governor.js +6 -3
- package/template/coding-task-manager/lib/typing-interaction-trace.js +92 -0
- package/template/coding-task-manager/lib/typing-worker-stages.js +60 -0
- package/template/coding-task-manager/lib/visible-revision-watchdog.js +8 -12
- package/template/coding-task-manager/lib/wake-skip-reasons.js +46 -0
- package/template/coding-task-manager/lib/walle-supervisor.js +98 -14
- package/template/coding-task-manager/lib/worker-failure-classify.js +13 -1
- package/template/coding-task-manager/lib/ws/state-sync.js +19 -2
- package/template/coding-task-manager/lib/ws/terminal-attach.js +32 -1
- package/template/coding-task-manager/lib/ws-router.js +68 -1
- package/template/coding-task-manager/lib/ws-send-admission.js +22 -4
- package/template/coding-task-manager/oauth-proxy.js +99 -55
- package/template/coding-task-manager/package-lock.json +24 -0
- package/template/coding-task-manager/package.json +3 -0
- package/template/coding-task-manager/providers/codex-mcp-extract.js +121 -19
- package/template/coding-task-manager/providers/codex-mcp.js +14 -5
- package/template/coding-task-manager/public/css/app.css +279 -1
- package/template/coding-task-manager/public/css/reviews.css +4 -0
- package/template/coding-task-manager/public/index.html +294 -223
- package/template/coding-task-manager/public/js/app/machines-page.js +1252 -0
- package/template/coding-task-manager/public/js/app/models-tab.js +546 -36
- package/template/coding-task-manager/public/js/app/worktrees-panel.js +7 -5
- package/template/coding-task-manager/public/js/background-work.js +13 -7
- package/template/coding-task-manager/public/js/coding-session-model-presentation.js +44 -4
- package/template/coding-task-manager/public/js/core/session-commands.js +676 -0
- package/template/coding-task-manager/public/js/core/session-selectors.js +5 -1
- package/template/coding-task-manager/public/js/history-app-seam.js +26 -2
- package/template/coding-task-manager/public/js/history-painted-rows.js +17 -2
- package/template/coding-task-manager/public/js/history-reader-park.js +13 -1
- package/template/coding-task-manager/public/js/history-scroller.js +187 -9
- package/template/coding-task-manager/public/js/history-transcript-pages.js +5 -5
- package/template/coding-task-manager/public/js/machine-pool-shared.js +436 -0
- package/template/coding-task-manager/public/js/private-state-scope.js +31 -3
- package/template/coding-task-manager/public/js/prompt-submission-client.js +173 -8
- package/template/coding-task-manager/public/js/prompt-submission-tray.js +281 -0
- package/template/coding-task-manager/public/js/queue-panel-submission-reconcile.js +174 -1
- package/template/coding-task-manager/public/js/reviews.js +7 -3
- package/template/coding-task-manager/public/js/root-draft-lifetime.js +139 -2
- package/template/coding-task-manager/public/js/root-queue-builder-lifetime.js +16 -0
- package/template/coding-task-manager/public/js/row-join.js +4 -2
- package/template/coding-task-manager/public/js/session-model-picker.js +1 -1
- package/template/coding-task-manager/public/js/session-search-utils.js +46 -19
- package/template/coding-task-manager/public/js/state-sync-client.js +4 -0
- package/template/coding-task-manager/public/js/terminal/runtime-02.js +16 -7
- package/template/coding-task-manager/public/js/terminal/runtime-03.js +41 -0
- package/template/coding-task-manager/public/js/terminal/runtime-04.js +41 -4
- package/template/coding-task-manager/public/js/terminal/runtime-12.js +52 -2
- package/template/coding-task-manager/public/js/terminal/runtime-16.js +12 -12
- package/template/coding-task-manager/public/js/terminal/runtime-18.js +14 -0
- package/template/coding-task-manager/public/js/terminal/runtime-19.js +8 -4
- package/template/coding-task-manager/public/js/terminal/runtime-20.js +210 -73
- package/template/coding-task-manager/public/js/terminal/runtime-21.js +19 -6
- package/template/coding-task-manager/public/js/terminal/runtime-22.js +86 -0
- package/template/coding-task-manager/public/js/terminal-history-viewport.js +1 -1
- package/template/coding-task-manager/public/js/terminal-wheel.js +39 -2
- package/template/coding-task-manager/public/js/walle/chat-conversation.js +11 -3
- package/template/coding-task-manager/public/js/walle-session.js +30 -7
- package/template/coding-task-manager/public/m/app.css +226 -8
- package/template/coding-task-manager/public/m/app.js +502 -9
- package/template/coding-task-manager/public/m/index.html +38 -1
- package/template/coding-task-manager/public/m/machines.js +1313 -0
- package/template/coding-task-manager/public/m/pool-failover.js +391 -0
- package/template/coding-task-manager/public/m/private-queue-actions.js +26 -7
- package/template/coding-task-manager/public/m/sw.js +5 -0
- package/template/coding-task-manager/queue-engine.js +13 -0
- package/template/coding-task-manager/server/approvals.js +5 -6
- package/template/coding-task-manager/server/boot.js +136 -17
- package/template/coding-task-manager/server.js +902 -239
- package/template/coding-task-manager/session-integrity.js +7 -9
- package/template/coding-task-manager/workers/db-owner-worker.js +53 -7
- package/template/coding-task-manager/workers/read-pool-worker.js +17 -6
- package/template/coding-task-manager/workers/session-host-pool-process.js +18 -4
- package/template/coding-task-manager/workers/session-host-process.js +14 -3
- package/template/coding-task-manager/workers/transcript-archive-worker.js +1 -1
- package/template/coding-task-manager/workers/worktree-worker.js +34 -0
- package/template/package-lock.json +2 -2
- package/template/package.json +1 -1
- package/template/scripts/conversation-storage-v2-migration.js +78 -11
- package/template/shared/conversation-storage-v2-contract.js +42 -6
- package/template/shared/fixtures/worker-status-payloads.json +392 -0
- package/template/shared/git-remote-key.js +83 -0
- package/template/shared/host-identity.js +98 -2
- package/template/shared/owner-write-safety.js +135 -0
- package/template/shared/public-runtime.js +13 -5
- package/template/shared/release-schema.json +3 -2
- package/template/shared/report-citations.js +14 -0
- package/template/shared/sqlite-write-lock.js +361 -223
- package/template/shared/storage/contract.js +35 -3
- package/template/wall-e/agent.js +21 -0
- package/template/wall-e/api-walle.js +5 -0
- package/template/wall-e/bin/brain-purge-legacy-ctm.js +3 -2
- package/template/wall-e/bin/walle-mcp-stdio.js +12 -6
- package/template/wall-e/brain/helper-rules-schema.js +129 -0
- package/template/wall-e/brain/migrations.js +51 -22
- package/template/wall-e/brain/model-deployment-facts.js +31 -1
- package/template/wall-e/brain/owner-call-whitelist.js +16 -0
- package/template/wall-e/brain/schema-ensures.js +22 -0
- package/template/wall-e/brain.js +1 -1
- package/template/wall-e/coding/journal-dispatcher.js +51 -0
- package/template/wall-e/coding/session-journal.js +28 -40
- package/template/wall-e/coding-orchestrator.js +23 -26
- package/template/wall-e/http/auth.js +18 -1
- package/template/wall-e/http/blog-views-api.js +318 -0
- package/template/wall-e/http/helper-rules-api.js +321 -0
- package/template/wall-e/http/model-proxy-providers.js +3 -1
- package/template/wall-e/http/telemetry-api.js +12 -0
- package/template/wall-e/lib/blog-view-policy.js +88 -0
- package/template/wall-e/lib/blog-view-store.js +169 -0
- package/template/wall-e/lib/client-config-lock.js +2 -1
- package/template/wall-e/lib/coding-journal-writer-child-client.js +67 -18
- package/template/wall-e/lib/error-kinds.js +10 -1
- package/template/wall-e/lib/feature-manifest.js +1 -1
- package/template/wall-e/lib/helper-rules/cell-state.js +123 -0
- package/template/wall-e/lib/helper-rules/evaluate.js +357 -0
- package/template/wall-e/lib/helper-rules/explore-rubric.md +12 -0
- package/template/wall-e/lib/helper-rules/grade.js +277 -0
- package/template/wall-e/lib/helper-rules/label.js +247 -0
- package/template/wall-e/lib/helper-rules/lead-call-hook.js +522 -0
- package/template/wall-e/lib/helper-rules/store.js +667 -0
- package/template/wall-e/lib/mcp-integration.js +5 -6
- package/template/wall-e/lib/runtime-worker-pool.js +1 -0
- package/template/wall-e/lib/scheduler-worker-jobs.js +6 -0
- package/template/wall-e/lib/telemetry-db-schema.js +37 -0
- package/template/wall-e/lib/telemetry-process-client.js +76 -1
- package/template/wall-e/llm/default-fallback.js +4 -0
- package/template/wall-e/llm/model-catalog.js +13 -4
- package/template/wall-e/llm/model-roles.js +6 -0
- package/template/wall-e/llm/portkey-http.js +150 -0
- package/template/wall-e/llm/portkey-sync.js +15 -145
- package/template/wall-e/llm/worker-mcp-routing.js +61 -0
- package/template/wall-e/llm/worker-route.js +43 -1
- package/template/wall-e/loops/brain-custom-writes.js +45 -0
- package/template/wall-e/mcp-protocol-contract.js +41 -1
- package/template/wall-e/mcp-server.js +13 -2
- package/template/wall-e/mcp-worker-schemas.js +20 -2
- package/template/wall-e/memory/ctm-context-client.js +25 -5
- package/template/wall-e/shared/release-schema.json +3 -2
- package/template/wall-e/shared/sqlite-write-lock.js +361 -223
- package/template/wall-e/shared/storage/contract.js +35 -3
- package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
- package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +6 -0
- package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +7 -2
- package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +7 -2
- package/template/wall-e/tools/local-tools.js +6 -4
- package/template/wall-e/workers/brain-owner-worker.js +9 -0
- package/template/wall-e/workers/coding-journal-writer-child.js +8 -1
- package/template/wall-e/workers/telemetry-worker.js +8 -0
|
@@ -160,6 +160,13 @@
|
|
|
160
160
|
itemElements: [],
|
|
161
161
|
},
|
|
162
162
|
projects: [],
|
|
163
|
+
// WP-5 (S-16/AC-5c): the machine registry, mirrored field for field from the desktop client —
|
|
164
|
+
// same GET /api/machines rows, same reachable/unknown/unhealthy card states, same chip facts.
|
|
165
|
+
// Empty until the hub answers; with zero machines nothing machine-shaped renders at all.
|
|
166
|
+
machines: [],
|
|
167
|
+
// The project picked on this Mac before a box was chosen in the new-session sheet: the key the
|
|
168
|
+
// same-repo prefill matches, and what "This Mac" restores.
|
|
169
|
+
newSessionLocalCwd: '',
|
|
163
170
|
pendingStepUp: null,
|
|
164
171
|
workflowRoute: { pending: null },
|
|
165
172
|
homeSearchQuery: '',
|
|
@@ -386,7 +393,7 @@
|
|
|
386
393
|
|| !mobilePrivateStateRuntime.matchesAuth(operation.principalAuth, lease)) {
|
|
387
394
|
if (mobilePrivatePreparationOperation !== operation) return null;
|
|
388
395
|
identityCache.clear(); mobilePrivateLease = null; mobilePrivateReady = false; mobilePrivateCache = null; // D-7: an unattached lease keeps no identity bytes
|
|
389
|
-
state.sessions = []; state.sessionsLoaded = true; state.remoteOutbox = [];
|
|
396
|
+
state.sessions = []; state.sessionsLoaded = true; state.remoteOutbox = []; syncPoolOutboxPendingMarker();
|
|
390
397
|
state.privateScopeError = mobilePrivateStateRuntime.snapshot()?.code || 'private_scope_unavailable';
|
|
391
398
|
setConnection('offline', 'Private data paused');
|
|
392
399
|
render();
|
|
@@ -420,7 +427,7 @@
|
|
|
420
427
|
const staleSocket = state.ws; state.ws = null; state.wsReady = false; clearWsReconnectTimer(); clearWsLivenessTimers(); try { staleSocket?.close(); } catch { /* expected: invalidation can race socket close */ }
|
|
421
428
|
clearTimeout(state.remoteOutboxTimer); state.remoteOutboxTimer = null;
|
|
422
429
|
clearTimeout(state.remoteOutboxStorage.persistTimer); state.remoteOutboxStorage.persistTimer = null;
|
|
423
|
-
state.remoteOutbox = []; state.remoteOutboxDraining = false; state.sessions = []; state.sessionsLoaded = true; state.sessionsFromCache = false;
|
|
430
|
+
state.remoteOutbox = []; syncPoolOutboxPendingMarker(); state.remoteOutboxDraining = false; state.sessions = []; state.sessionsLoaded = true; state.sessionsFromCache = false;
|
|
424
431
|
state.activeSession = null; state.detail.sessionId = null; state.detail.messages = []; state.remoteSubmissions.clear();
|
|
425
432
|
state.remote = null; state.remoteStatusPromise = null; state.remoteStatusLoading = false; remoteStatusLease = null;
|
|
426
433
|
state.remoteOutboxStorage.persistInFlight = null; remoteOutboxMirrorDeferred = false;
|
|
@@ -2458,6 +2465,7 @@
|
|
|
2458
2465
|
<span class="session-title-line">
|
|
2459
2466
|
<span class="session-title">${esc(card.title || card.id || 'Session')}</span>
|
|
2460
2467
|
${opts.worker === true ? cardModelChip(card) : ''}
|
|
2468
|
+
${phoneMachineChip(card)}
|
|
2461
2469
|
${pill}
|
|
2462
2470
|
</span>
|
|
2463
2471
|
<span class="session-meta"><span class="session-meta-text">${esc(meta)}</span>${sessionDiffMark(card)}</span>
|
|
@@ -4099,6 +4107,10 @@
|
|
|
4099
4107
|
.then((mod) => {
|
|
4100
4108
|
if (container) container.dataset.moreLoaded = '1';
|
|
4101
4109
|
mod.renderMore(moreContext());
|
|
4110
|
+
// WP-5 (D-11/S-16): the Machines card is part of More on the phone, exactly as it is part of
|
|
4111
|
+
// Settings on the desktop. more.js owns #more-content and rewrites it wholesale, so the card
|
|
4112
|
+
// is (re)appended after each of its renders.
|
|
4113
|
+
renderPhoneMachinesCard();
|
|
4102
4114
|
})
|
|
4103
4115
|
.catch((err) => {
|
|
4104
4116
|
if (container) {
|
|
@@ -5616,7 +5628,25 @@
|
|
|
5616
5628
|
}
|
|
5617
5629
|
}
|
|
5618
5630
|
|
|
5631
|
+
// WP-4 (docs/dgx-spark-coding-readiness-2026-10-24.html) — phone = desktop parity: the desktop
|
|
5632
|
+
// shows a MACHINE-scoped failure frame verbatim in a toast (public/index.html:4471-4481), while
|
|
5633
|
+
// this dispatcher used to drop every 'error' frame, so a hub refusal (machine_agent_signed_out,
|
|
5634
|
+
// machine_unreachable, …) was invisible on the phone. Only machine failures are shown, through
|
|
5635
|
+
// the app's existing toast helper (:13662); every other error frame keeps today's behaviour.
|
|
5636
|
+
function showMobileMachineError(msg) {
|
|
5637
|
+
const code = String((msg && msg.code) || '');
|
|
5638
|
+
if (!/^machine_/.test(code) && !(msg && msg.machine)) return false;
|
|
5639
|
+
toast(String((msg && (msg.message || msg.error)) || code || 'Machine error'));
|
|
5640
|
+
return true;
|
|
5641
|
+
}
|
|
5642
|
+
|
|
5619
5643
|
function dispatchMobileWsMessage(msg) {
|
|
5644
|
+
if (msg.type === 'error') {
|
|
5645
|
+
// A machine create refused at the hub names the box and the page that fixes it; show it and
|
|
5646
|
+
// stop. 'hello'/'pong'/… are unaffected, and no other frame type is touched.
|
|
5647
|
+
showMobileMachineError(msg);
|
|
5648
|
+
return;
|
|
5649
|
+
}
|
|
5620
5650
|
if (msg.type === 'hello') {
|
|
5621
5651
|
handleServerHello(msg);
|
|
5622
5652
|
return;
|
|
@@ -5667,9 +5697,16 @@
|
|
|
5667
5697
|
}
|
|
5668
5698
|
if (msg.type === 'sessions' || msg.type === 'data-changed') {
|
|
5669
5699
|
if (msg.type === 'data-changed' && ['models', 'model-registry', 'providers'].includes(msg.resource)) phoneModelPicker?.invalidateCatalog?.(); // D-6: as desktop onDataChanged
|
|
5700
|
+
// R-3: a queue-draft echo reloads the open idle builder's draft (reloadIfIdle guards
|
|
5701
|
+
// mid-operation panes); scheduleRefresh below is unchanged.
|
|
5702
|
+
if (msg.type === 'data-changed' && msg.resource === 'queue-draft' && msg.id) {
|
|
5703
|
+
privateQueueActions.reloadIfIdle(msg.id).catch(window.ClientErrors.tail('mobile:queue-draft-load'));
|
|
5704
|
+
}
|
|
5670
5705
|
scheduleRefresh(350);
|
|
5671
5706
|
return;
|
|
5672
5707
|
}
|
|
5708
|
+
// R-3: delivery transitions (server.js broadcastQueueState) were dropped on the phone.
|
|
5709
|
+
if (msg.type === 'queue-state' && msg.sessionId) privateQueueActions.reloadIfIdle(msg.sessionId).catch(window.ClientErrors.tail('mobile:queue-draft-load'));
|
|
5673
5710
|
if (msg.type === 'session-activity') { applySessionActivity(msg); return; }
|
|
5674
5711
|
if (msg.type === 'tunnel-economy') { phonePresence.handleTunnelEconomy(msg); return; } // D-8: the lane governor's economy notice drives the passive #detail-tunnel-economy chip
|
|
5675
5712
|
if (msg.type === 'session.status') {
|
|
@@ -5814,7 +5851,28 @@
|
|
|
5814
5851
|
}, 0);
|
|
5815
5852
|
}
|
|
5816
5853
|
|
|
5854
|
+
// Plain window.localStorage, or null when the context blocks it (private mode throws on the getter
|
|
5855
|
+
// itself). The outward mirror of the queue is the only thing in this file that uses plain storage.
|
|
5856
|
+
function phonePlainStorage() {
|
|
5857
|
+
try {
|
|
5858
|
+
return window.localStorage || null;
|
|
5859
|
+
} catch (error) {
|
|
5860
|
+
return null; // expected: a blocked/opaque context throws on the localStorage getter itself
|
|
5861
|
+
}
|
|
5862
|
+
}
|
|
5863
|
+
|
|
5864
|
+
// P-7 boot seam: public/m/pool-failover.js probes the other members before this app has a private
|
|
5865
|
+
// lease, and cannot read the outbox (which is leased). So every persist and every clear mirrors ONLY
|
|
5866
|
+
// the queue's presence into `ctm.pool.outboxPending` — '1' or absent, never an entry, an id or a
|
|
5867
|
+
// count — and attemptPhonePoolFailover() reads it back through the shared module.
|
|
5868
|
+
function syncPoolOutboxPendingMarker() {
|
|
5869
|
+
const failover = window.CtmPoolFailover;
|
|
5870
|
+
if (!failover?.writeOutboxPendingMarker) return false;
|
|
5871
|
+
return failover.writeOutboxPendingMarker(phonePlainStorage(), state.remoteOutbox.length > 0);
|
|
5872
|
+
}
|
|
5873
|
+
|
|
5817
5874
|
function persistRemoteOutbox(options = {}) {
|
|
5875
|
+
syncPoolOutboxPendingMarker();
|
|
5818
5876
|
const captured = capturedPrivateStorage(options.lease || mobilePrivateLease);
|
|
5819
5877
|
if (!captured || !remoteOutboxPersistence) return false;
|
|
5820
5878
|
if (pendingDurableOutboxWrites.size && options.includePending !== true) {
|
|
@@ -6087,6 +6145,7 @@
|
|
|
6087
6145
|
} catch (err) {
|
|
6088
6146
|
const index = state.remoteOutbox.findIndex((item) => item === entry && item.id === entry.id);
|
|
6089
6147
|
if (index >= 0) state.remoteOutbox.splice(index, 1);
|
|
6148
|
+
syncPoolOutboxPendingMarker();
|
|
6090
6149
|
throw err;
|
|
6091
6150
|
} finally { finishPendingDurableOutboxWrite(entry.id, lease); }
|
|
6092
6151
|
if (typeof opts.isStatusCurrent !== 'function' || opts.isStatusCurrent()) setComposerStatus(remoteOutboxQueuedStatus(entry), '');
|
|
@@ -13284,19 +13343,357 @@
|
|
|
13284
13343
|
if (state.activeTab === 'search') renderSearch();
|
|
13285
13344
|
}
|
|
13286
13345
|
|
|
13287
|
-
|
|
13346
|
+
// ── WP-5 machine surfaces (docs/remote-machine-hub-sessions-2026-10-22.html §6.6, D-11, S-16) ──
|
|
13347
|
+
// The desktop presenter's field-for-field twin (public/js/core/session-commands.js): the same
|
|
13348
|
+
// /api/machines rows, the same card-state table, the same chip text and the same payload field.
|
|
13349
|
+
// Phone = desktop parity: no phone-only machine feature and no desktop-only machine feature.
|
|
13350
|
+
const PHONE_MACHINE_CARD_STATES = Object.freeze(['reachable', 'unknown', 'unhealthy']);
|
|
13351
|
+
|
|
13352
|
+
function phoneMachineFacts(row) {
|
|
13353
|
+
const meta = (row && (row.meta || row)) || {};
|
|
13354
|
+
return {
|
|
13355
|
+
machine: String(meta.machine || '').trim(),
|
|
13356
|
+
name: String(meta.machineName || meta.machine_name || '').trim(),
|
|
13357
|
+
health: String(meta.machineHealth || meta.machine_health || '').trim(),
|
|
13358
|
+
};
|
|
13359
|
+
}
|
|
13360
|
+
|
|
13361
|
+
// '' for a local session, so local rows are byte-identical to their pre-hub render (AC-5b).
|
|
13362
|
+
function phoneMachineChip(row) {
|
|
13363
|
+
const facts = phoneMachineFacts(row);
|
|
13364
|
+
if (!facts.machine) return '';
|
|
13365
|
+
const name = facts.name || facts.machine;
|
|
13366
|
+
const unreachable = facts.health === 'unreachable';
|
|
13367
|
+
const title = unreachable ? `Runs on ${name} — unreachable (showing its last known state)` : `Runs on ${name}`;
|
|
13368
|
+
return `<span class="machine-chip${unreachable ? ' unreachable' : ''}" title="${esc(title)}" aria-label="${esc(title)}"`
|
|
13369
|
+
+ ` data-machine-name="${esc(name)}" data-machine-health="${esc(facts.health)}">${esc((unreachable ? '⚠ ' : '◈ ') + name)}</span>`;
|
|
13370
|
+
}
|
|
13371
|
+
|
|
13372
|
+
// WP-4 (docs/dgx-spark-coding-readiness-2026-10-24.html): the same rule as the desktop presenter —
|
|
13373
|
+
// a reachable box whose Claude/Codex CLI is signed out names the agents it needs signed in
|
|
13374
|
+
// (`agents` = the hub's last-read {claude, codex, checkedAt} verdict). The option stays ENABLED:
|
|
13375
|
+
// shell sessions still work there and the hub refuses a signed-out agent create itself.
|
|
13376
|
+
const PHONE_MACHINE_AGENT_LABELS = { claude: 'Claude', codex: 'Codex' };
|
|
13377
|
+
|
|
13378
|
+
function phoneMachineSignedOutAgents(machine) {
|
|
13379
|
+
const agents = machine && machine.agents;
|
|
13380
|
+
if (!agents || typeof agents !== 'object') return [];
|
|
13381
|
+
return ['claude', 'codex']
|
|
13382
|
+
.filter((key) => agents[key] === 'signed_out')
|
|
13383
|
+
.map((key) => PHONE_MACHINE_AGENT_LABELS[key]);
|
|
13384
|
+
}
|
|
13385
|
+
|
|
13386
|
+
// WP-4: the machine's NAME, read from the registry row (state.machines, the same array
|
|
13387
|
+
// phoneMachineOptionsHtml is called with — desktop machineSelectionName() does exactly this),
|
|
13388
|
+
// never from the option's rendered text, which carries the "◈ " glyph and may carry a
|
|
13389
|
+
// "— sign in: …" suffix. An id the list does not know falls back to the raw id.
|
|
13390
|
+
function phoneMachineDisplayName(machineId) {
|
|
13391
|
+
const id = String(machineId || '').trim();
|
|
13392
|
+
if (!id) return '';
|
|
13393
|
+
const row = (Array.isArray(state.machines) ? state.machines : [])
|
|
13394
|
+
.find((machine) => machine && String(machine.instanceId || '') === id);
|
|
13395
|
+
return String((row && (row.displayName || row.url)) || id);
|
|
13396
|
+
}
|
|
13397
|
+
|
|
13398
|
+
// AC-5a: reachable is selectable; unknown/unhealthy render DISABLED with their reason;
|
|
13399
|
+
// unreachable is ABSENT. The phone expresses the same three states as <option> attributes.
|
|
13400
|
+
function phoneMachineCardView(machine) {
|
|
13401
|
+
const health = String((machine && machine.health) || '').trim();
|
|
13402
|
+
if (PHONE_MACHINE_CARD_STATES.indexOf(health) === -1) return null;
|
|
13403
|
+
const id = String((machine && machine.instanceId) || '');
|
|
13404
|
+
if (!id) return null;
|
|
13405
|
+
const name = machine.displayName || machine.url || id;
|
|
13406
|
+
const reason = health === 'reachable'
|
|
13407
|
+
? 'reachable'
|
|
13408
|
+
: health === 'unknown'
|
|
13409
|
+
? 'health unknown — the box is not answering probes'
|
|
13410
|
+
: 'unhealthy — the box is up but its hub is degraded';
|
|
13411
|
+
const view = { id, name, health, reason, disabled: health !== 'reachable' };
|
|
13412
|
+
// WP-4: only a reachable box names an agent — a disabled option keeps its single reason.
|
|
13413
|
+
if (health === 'reachable') {
|
|
13414
|
+
const signInNeeded = phoneMachineSignedOutAgents(machine);
|
|
13415
|
+
if (signInNeeded.length) view.signInNeeded = signInNeeded;
|
|
13416
|
+
}
|
|
13417
|
+
return view;
|
|
13418
|
+
}
|
|
13419
|
+
|
|
13420
|
+
// The hub's registry: the phone reads the same GET /api/machines the desktop does. A hub-less
|
|
13421
|
+
// (older) server 404s and the phone simply has no machines (no grid, no chip, no card).
|
|
13422
|
+
async function loadMachines() {
|
|
13423
|
+
try {
|
|
13424
|
+
const payload = await api('/api/machines');
|
|
13425
|
+
state.machines = Array.isArray(payload?.machines) ? payload.machines.filter((m) => m && m.instanceId) : [];
|
|
13426
|
+
} catch {
|
|
13427
|
+
state.machines = [];
|
|
13428
|
+
}
|
|
13429
|
+
return state.machines;
|
|
13430
|
+
}
|
|
13431
|
+
|
|
13432
|
+
// The desktop rule (session-commands.js machinesWritableHere): registration is loopback-only
|
|
13433
|
+
// server-side, so off loopback the phone shows the list read-only instead of a form that 403s.
|
|
13434
|
+
// The phone's own loopback signal is the auth principal's isLoopback.
|
|
13435
|
+
function machinesWritableHere() {
|
|
13436
|
+
return state.auth?.isLoopback === true;
|
|
13437
|
+
}
|
|
13438
|
+
|
|
13439
|
+
// WP-6 (brief item 2): the phone Machines page is public/m/machines.js — the twin of the desktop
|
|
13440
|
+
// machines-page.js (This Mac + every registry member, the same groups/badges/sparklines/actions,
|
|
13441
|
+
// the same Add dialog, Show a code, Leave pool, Join pool, New session here, "Sign in to <member>
|
|
13442
|
+
// too"). The old phone card here (register form, remove button, health line) is DELETED, not kept
|
|
13443
|
+
// beside it: the register form's legacy URL path lives in the Add dialog on both surfaces, exactly
|
|
13444
|
+
// as WP-5 has it on the desktop.
|
|
13445
|
+
let phoneMachinesSurface = null;
|
|
13446
|
+
|
|
13447
|
+
function phoneMachines() {
|
|
13448
|
+
if (phoneMachinesSurface) return phoneMachinesSurface;
|
|
13449
|
+
phoneMachinesSurface = window.CtmPhoneMachines?.create?.({
|
|
13450
|
+
$,
|
|
13451
|
+
document,
|
|
13452
|
+
esc,
|
|
13453
|
+
api,
|
|
13454
|
+
toast,
|
|
13455
|
+
loadMachines: (options) => loadMachines(options),
|
|
13456
|
+
openNewSessionSheet: (instanceId) => openNewSessionMachine(instanceId),
|
|
13457
|
+
confirm: (message) => window.confirm(message),
|
|
13458
|
+
now: () => Date.now(),
|
|
13459
|
+
}) || null;
|
|
13460
|
+
return phoneMachinesSurface;
|
|
13461
|
+
}
|
|
13462
|
+
|
|
13463
|
+
// Renders (or re-renders) the Machines section inside #more-content. more.js owns that container and
|
|
13464
|
+
// rewrites it wholesale, so this runs again after each of its renders and the section node is reused.
|
|
13465
|
+
function renderPhoneMachinesCard() {
|
|
13466
|
+
return phoneMachines()?.render($('more-content')) || null;
|
|
13467
|
+
}
|
|
13468
|
+
|
|
13469
|
+
// ── WP-6 item 3: the picker — <select> at N <= 4, the search sheet above it ─────────────────────
|
|
13470
|
+
// The one clock read for the picker (D-12 staleness). Named, so a test drives it instead of the
|
|
13471
|
+
// page's real clock.
|
|
13472
|
+
function phoneMachinesNow() {
|
|
13473
|
+
return Date.now();
|
|
13474
|
+
}
|
|
13475
|
+
// N = This Mac + the visible registry rows (D-1). At or below MACHINE_PICKER_CARD_LIMIT the phone
|
|
13476
|
+
// keeps the one-tap <select>: its option text carries the D-12 load line when the hub has a fresh
|
|
13477
|
+
// sample, because a phone option row is short enough for it ("◈ Spark — CPU 12% · 3 running");
|
|
13478
|
+
// when the sample is stale or absent the option keeps the pre-WP-6 "◈ Name" text, so a hub that
|
|
13479
|
+
// sends no samples renders exactly what it rendered before. Above the limit the picker is the
|
|
13480
|
+
// search sheet (D-2 search, D-3 groups, offline disabled with "last seen …").
|
|
13481
|
+
function phoneMachineOptionsHtml(machines, now) {
|
|
13482
|
+
const views = (Array.isArray(machines) ? machines : []).filter((m) => m && m.instanceId).map(phoneMachineCardView).filter(Boolean);
|
|
13483
|
+
const items = ['<option value="">This Mac</option>']
|
|
13484
|
+
.concat(views.map((view) => {
|
|
13485
|
+
// '' when nothing is signed out, so the option text is byte-identical to its pre-WP-4 self.
|
|
13486
|
+
const signIn = !view.disabled && Array.isArray(view.signInNeeded) && view.signInNeeded.length
|
|
13487
|
+
? ` — sign in: ${view.signInNeeded.join(', ')}`
|
|
13488
|
+
: '';
|
|
13489
|
+
const machine = (Array.isArray(machines) ? machines : []).find((m) => String(m?.instanceId || '') === view.id);
|
|
13490
|
+
// machineLoadLine is the shared copy (public/js/machine-pool-shared.js), loaded by
|
|
13491
|
+
// public/m/index.html before this file — the same global the desktop picker calls.
|
|
13492
|
+
const load = !view.disabled && machine ? machineLoadLine(machine, now) : '';
|
|
13493
|
+
// A stale/absent sample yields 'stats stale' or '' — neither earns a place in a one-line option.
|
|
13494
|
+
const loadText = load && load !== 'stats stale' ? ` — ${load}` : '';
|
|
13495
|
+
return `<option value="${esc(view.id)}"${view.disabled ? ' disabled' : ''}>${esc(view.disabled ? `◈ ${view.name} — ${view.reason}` : `◈ ${view.name}${loadText}${signIn}`)}</option>`;
|
|
13496
|
+
}));
|
|
13497
|
+
return { views, html: items.join('') };
|
|
13498
|
+
}
|
|
13499
|
+
|
|
13500
|
+
function phoneMachinePickerMode(machines) {
|
|
13501
|
+
return window.CtmPhoneMachines?.pickerMode?.(machines) || 'select';
|
|
13502
|
+
}
|
|
13503
|
+
|
|
13504
|
+
async function openNewSessionSheet() {
|
|
13288
13505
|
if (!state.auth?.scopes?.includes('create') && !state.auth?.scopes?.includes('admin')) {
|
|
13289
13506
|
toast('This phone token needs create scope. Upgrade it on the Mac.');
|
|
13290
13507
|
return;
|
|
13291
13508
|
}
|
|
13292
13509
|
const select = $('new-session-project');
|
|
13293
|
-
|
|
13294
|
-
|
|
13295
|
-
|
|
13296
|
-
|
|
13510
|
+
select.innerHTML = phoneProjectOptionsHtml(state.projects);
|
|
13511
|
+
state.newSessionLocalCwd = '';
|
|
13512
|
+
phoneMachineSetHint('');
|
|
13513
|
+
// WP-5 (S-16/AC-5c): the machine choice is fetched per open, exactly like the desktop modal.
|
|
13514
|
+
// With zero renderable machines the field stays hidden and the sheet is unchanged (AC-5a).
|
|
13515
|
+
// WP-6 (D-1/D-2/D-3): above MACHINE_PICKER_CARD_LIMIT machines the field becomes the search sheet
|
|
13516
|
+
// — the <select> stays as the value store (hidden), and the row above it opens the sheet.
|
|
13517
|
+
const machineField = $('new-session-machine-field');
|
|
13518
|
+
const machineSelect = $('new-session-machine');
|
|
13519
|
+
const machineRow = $('new-session-machine-row');
|
|
13520
|
+
if (machineSelect) machineSelect.innerHTML = '<option value="">This Mac</option>';
|
|
13521
|
+
if (machineField) machineField.hidden = true;
|
|
13522
|
+
const machines = await loadMachines();
|
|
13523
|
+
if (machineSelect) {
|
|
13524
|
+
const { views, html } = phoneMachineOptionsHtml(machines, phoneMachinesNow());
|
|
13525
|
+
machineSelect.innerHTML = html;
|
|
13526
|
+
if (machineField) machineField.hidden = views.length === 0;
|
|
13527
|
+
phoneMachineSetPickerMode(views.length ? phoneMachinePickerMode(machines) : 'select');
|
|
13528
|
+
}
|
|
13529
|
+
if (machineRow) machineRow.hidden = true;
|
|
13297
13530
|
$('new-session-sheet').hidden = false;
|
|
13298
13531
|
}
|
|
13299
13532
|
|
|
13533
|
+
// The picker's mode decides which of the two controls is visible; the <select> is always the value
|
|
13534
|
+
// holder, so createSession's `$('new-session-machine').value` read never changes.
|
|
13535
|
+
function phoneMachineSetPickerMode(mode) {
|
|
13536
|
+
const search = mode === 'search';
|
|
13537
|
+
const machineSelect = $('new-session-machine');
|
|
13538
|
+
const machineRow = $('new-session-machine-row');
|
|
13539
|
+
if (machineSelect) machineSelect.hidden = search;
|
|
13540
|
+
if (machineRow) {
|
|
13541
|
+
machineRow.hidden = !search;
|
|
13542
|
+
machineRow.textContent = phoneMachineSelectedLabel();
|
|
13543
|
+
}
|
|
13544
|
+
return search;
|
|
13545
|
+
}
|
|
13546
|
+
|
|
13547
|
+
function phoneMachineSelectedLabel() {
|
|
13548
|
+
const id = String($('new-session-machine')?.value || '').trim();
|
|
13549
|
+
return id ? `◈ ${phoneMachineDisplayName(id)}` : '◈ This Mac';
|
|
13550
|
+
}
|
|
13551
|
+
|
|
13552
|
+
// WP-6 item 4 / D-1: "New session here" from the Machines page opens the EXISTING new-session sheet
|
|
13553
|
+
// and selects that machine. One create path, exactly as the desktop page's machinesPageNewSessionHere.
|
|
13554
|
+
async function openNewSessionMachine(instanceId) {
|
|
13555
|
+
await openNewSessionSheet();
|
|
13556
|
+
const machineSelect = $('new-session-machine');
|
|
13557
|
+
const id = String(instanceId || '').trim();
|
|
13558
|
+
if (!machineSelect || machineSelect.hidden || !id) return false;
|
|
13559
|
+
machineSelect.value = id;
|
|
13560
|
+
phoneMachineSetPickerMode(phoneMachinePickerMode(state.machines));
|
|
13561
|
+
await onPhoneMachineChange();
|
|
13562
|
+
return true;
|
|
13563
|
+
}
|
|
13564
|
+
|
|
13565
|
+
// ── WP-6 item 3: the machine search sheet (N > MACHINE_PICKER_CARD_LIMIT) ───────────────────────
|
|
13566
|
+
let phoneMachineSheetOpen = false;
|
|
13567
|
+
|
|
13568
|
+
function renderPhoneMachineSearchSheet(query) {
|
|
13569
|
+
const list = $('machine-search-list');
|
|
13570
|
+
if (!list) return false;
|
|
13571
|
+
const now = phoneMachinesNow();
|
|
13572
|
+
const selected = String($('new-session-machine')?.value || '').trim();
|
|
13573
|
+
list.innerHTML = window.CtmPhoneMachines?.sheetListHtml?.(state.machines, query, selected, now) || '';
|
|
13574
|
+
const count = $('machine-search-count');
|
|
13575
|
+
if (count) {
|
|
13576
|
+
const groups = window.CtmPhoneMachines?.sheetGroups?.(state.machines, query, now) || [];
|
|
13577
|
+
const total = groups.reduce((sum, group) => sum + group.rows.length, 0);
|
|
13578
|
+
count.textContent = total
|
|
13579
|
+
? `${total} machine${total === 1 ? '' : 's'}`
|
|
13580
|
+
: 'No machine matches that search.';
|
|
13581
|
+
}
|
|
13582
|
+
return true;
|
|
13583
|
+
}
|
|
13584
|
+
|
|
13585
|
+
function openPhoneMachineSearchSheet() {
|
|
13586
|
+
const backdrop = $('machine-search-sheet');
|
|
13587
|
+
if (!backdrop) return false;
|
|
13588
|
+
const input = $('machine-search-input');
|
|
13589
|
+
if (input) input.value = '';
|
|
13590
|
+
renderPhoneMachineSearchSheet('');
|
|
13591
|
+
backdrop.hidden = false;
|
|
13592
|
+
phoneMachineSheetOpen = true;
|
|
13593
|
+
if (input && typeof input.focus === 'function') input.focus();
|
|
13594
|
+
return true;
|
|
13595
|
+
}
|
|
13596
|
+
|
|
13597
|
+
function closePhoneMachineSearchSheet() {
|
|
13598
|
+
const backdrop = $('machine-search-sheet');
|
|
13599
|
+
if (backdrop) backdrop.hidden = true;
|
|
13600
|
+
phoneMachineSheetOpen = false;
|
|
13601
|
+
return true;
|
|
13602
|
+
}
|
|
13603
|
+
|
|
13604
|
+
function bindPhoneMachineSearchSheet() {
|
|
13605
|
+
const input = $('machine-search-input');
|
|
13606
|
+
if (input) input.addEventListener('input', () => renderPhoneMachineSearchSheet(input.value));
|
|
13607
|
+
$('machine-search-close')?.addEventListener('click', () => closePhoneMachineSearchSheet());
|
|
13608
|
+
$('machine-search-sheet')?.addEventListener('click', (event) => {
|
|
13609
|
+
if (event.target === $('machine-search-sheet')) closePhoneMachineSearchSheet();
|
|
13610
|
+
});
|
|
13611
|
+
$('machine-search-list')?.addEventListener('click', async (event) => {
|
|
13612
|
+
const row = event.target?.closest?.('[data-machine-pick]');
|
|
13613
|
+
if (!row || row.disabled) return;
|
|
13614
|
+
event.preventDefault();
|
|
13615
|
+
const machineSelect = $('new-session-machine');
|
|
13616
|
+
if (!machineSelect) return;
|
|
13617
|
+
machineSelect.value = row.getAttribute('data-machine-pick') || '';
|
|
13618
|
+
phoneMachineSetPickerMode('search');
|
|
13619
|
+
closePhoneMachineSearchSheet();
|
|
13620
|
+
await onPhoneMachineChange();
|
|
13621
|
+
});
|
|
13622
|
+
$('new-session-machine-row')?.addEventListener('click', (event) => {
|
|
13623
|
+
event.preventDefault();
|
|
13624
|
+
openPhoneMachineSearchSheet();
|
|
13625
|
+
});
|
|
13626
|
+
}
|
|
13627
|
+
|
|
13628
|
+
function phoneProjectOptionsHtml(projects) {
|
|
13629
|
+
const rows = Array.isArray(projects) ? projects : [];
|
|
13630
|
+
return rows.length
|
|
13631
|
+
? rows.map((project) => `<option value="${esc(project.path || project.cwd || project.name)}">${esc(project.name || compactPath(project.path || project.cwd))}</option>`).join('')
|
|
13632
|
+
: '<option value="">Home</option>';
|
|
13633
|
+
}
|
|
13634
|
+
|
|
13635
|
+
function phoneMachineSetHint(text, warn) {
|
|
13636
|
+
const hint = $('new-session-machine-hint');
|
|
13637
|
+
if (!hint) return;
|
|
13638
|
+
hint.textContent = text || '';
|
|
13639
|
+
hint.hidden = !text;
|
|
13640
|
+
hint.classList.toggle('warn', !!warn);
|
|
13641
|
+
}
|
|
13642
|
+
|
|
13643
|
+
// The box checkout of the project picked on this Mac: the box row whose gitRemoteKey equals the
|
|
13644
|
+
// local project's key ('' = that project has no remote, so nothing can match).
|
|
13645
|
+
function phoneSameRepoProject(localProjects, localCwd, boxRows) {
|
|
13646
|
+
const local = (Array.isArray(localProjects) ? localProjects : []).find((project) => project && (project.path || project.cwd) === localCwd);
|
|
13647
|
+
const localKey = String(local?.gitRemoteKey || '');
|
|
13648
|
+
if (!localKey) return '';
|
|
13649
|
+
const match = (Array.isArray(boxRows) ? boxRows : []).find((row) => row && (row.path || row.cwd) && String(row.gitRemoteKey || '') === localKey);
|
|
13650
|
+
return match ? String(match.path || match.cwd) : '';
|
|
13651
|
+
}
|
|
13652
|
+
|
|
13653
|
+
// WP-5 (S-13, desktop _selectMachineCard + nsMachineAfterProjectsLoaded): the project list
|
|
13654
|
+
// belongs to the selected machine. A box lists the BOX's projects (GET /api/projects?machine=)
|
|
13655
|
+
// and prefills the checkout whose gitRemoteKey equals the key of the project picked on this Mac
|
|
13656
|
+
// (plain equality: the server normalizes); "This Mac" restores the local list and project. An
|
|
13657
|
+
// answer for a machine that is no longer selected is dropped, so a box path never lands under
|
|
13658
|
+
// "This Mac" (or under another box).
|
|
13659
|
+
async function onPhoneMachineChange() {
|
|
13660
|
+
const machineSelect = $('new-session-machine');
|
|
13661
|
+
const select = $('new-session-project');
|
|
13662
|
+
if (!machineSelect || !select) return;
|
|
13663
|
+
const machine = String(machineSelect.value || '').trim();
|
|
13664
|
+
if (!machine) {
|
|
13665
|
+
select.innerHTML = phoneProjectOptionsHtml(state.projects);
|
|
13666
|
+
if (state.newSessionLocalCwd) select.value = state.newSessionLocalCwd;
|
|
13667
|
+
state.newSessionLocalCwd = '';
|
|
13668
|
+
phoneMachineSetHint('');
|
|
13669
|
+
return;
|
|
13670
|
+
}
|
|
13671
|
+
if (!state.newSessionLocalCwd) state.newSessionLocalCwd = String(select.value || '');
|
|
13672
|
+
// The registry row's own name ('' only for an unknown id, which falls back to the id itself):
|
|
13673
|
+
// never the option's rendered text, which carries "◈ " and possibly "— sign in: …".
|
|
13674
|
+
const name = phoneMachineDisplayName(machine);
|
|
13675
|
+
select.innerHTML = '';
|
|
13676
|
+
phoneMachineSetHint(`Loading projects on ${name}…`);
|
|
13677
|
+
let rows = [];
|
|
13678
|
+
try {
|
|
13679
|
+
const data = await api(`/api/projects?machine=${encodeURIComponent(machine)}`);
|
|
13680
|
+
rows = Array.isArray(data) ? data : (Array.isArray(data?.projects) ? data.projects : []);
|
|
13681
|
+
} catch (err) {
|
|
13682
|
+
if (String($('new-session-machine')?.value || '').trim() !== machine) return;
|
|
13683
|
+
phoneMachineSetHint(`Could not list projects on ${name}: ${err?.message || err}`, true);
|
|
13684
|
+
return;
|
|
13685
|
+
}
|
|
13686
|
+
if (String($('new-session-machine')?.value || '').trim() !== machine) return;
|
|
13687
|
+
select.innerHTML = phoneProjectOptionsHtml(rows);
|
|
13688
|
+
const match = phoneSameRepoProject(state.projects, state.newSessionLocalCwd, rows);
|
|
13689
|
+
if (match) {
|
|
13690
|
+
select.value = match;
|
|
13691
|
+
phoneMachineSetHint(`Same repository found on ${name}.`);
|
|
13692
|
+
} else {
|
|
13693
|
+
phoneMachineSetHint(`No checkout on ${name} carries this project's git remote — pick a folder on that box.`, true);
|
|
13694
|
+
}
|
|
13695
|
+
}
|
|
13696
|
+
|
|
13300
13697
|
async function createSession(event) {
|
|
13301
13698
|
event.preventDefault();
|
|
13302
13699
|
const lease = mobilePrivateLease;
|
|
@@ -13317,11 +13714,15 @@
|
|
|
13317
13714
|
? 'walle'
|
|
13318
13715
|
: 'claude';
|
|
13319
13716
|
const args = prompt && !isWalle ? [prompt] : [];
|
|
13320
|
-
|
|
13717
|
+
// WP-5 (S-12/S-16): the machine chosen in the sheet travels with the create frame as `machine`;
|
|
13718
|
+
// '' is this Mac and the field is omitted, so a local create frame is unchanged.
|
|
13719
|
+
const machine = ($('new-session-machine')?.value || '').trim();
|
|
13720
|
+
const request = Object.freeze({ cwd, agent, agentLabel, prompt, isWalle, cmd, args: Object.freeze(args.slice()), machine });
|
|
13321
13721
|
const action = async () => {
|
|
13322
13722
|
if (!privateLeaseCurrent(lease) || state.pendingStepUp?.action !== action) throw privateScopeChangedError();
|
|
13323
13723
|
const id = randomId();
|
|
13324
13724
|
const payload = { type: 'create', id, cwd: request.cwd, cmd: request.cmd, args: request.args.slice() };
|
|
13725
|
+
if (request.machine) payload.machine = request.machine;
|
|
13325
13726
|
if (request.isWalle) {
|
|
13326
13727
|
Object.assign(payload, {
|
|
13327
13728
|
label: 'Wall-E Coding', agentType: 'walle', agentMode: 'coding', agentKind: 'walle-coding',
|
|
@@ -13368,6 +13769,7 @@
|
|
|
13368
13769
|
{ ...hiddenSheet('stepup-sheet'), close: () => $('stepup-cancel')?.click() },
|
|
13369
13770
|
{ ...hiddenSheet('workflow-route-sheet'), close: () => closeWorkflowRouteSheet('cancel') },
|
|
13370
13771
|
{ ...hiddenSheet('model-picker-sheet'), close: () => closeModelPicker() },
|
|
13772
|
+
{ ...hiddenSheet('machine-search-sheet'), close: () => closePhoneMachineSearchSheet() },
|
|
13371
13773
|
{ ...hiddenSheet('review-sheet'), close: () => closeReviewSheet() },
|
|
13372
13774
|
{ ...hiddenSheet('review-comment-sheet'), close: () => { $('review-comment-sheet').hidden = true; } },
|
|
13373
13775
|
{ ...hiddenSheet('queue-builder-sheet'), close: () => closeQueueBuilder() },
|
|
@@ -13768,6 +14170,8 @@
|
|
|
13768
14170
|
$('new-session-btn').addEventListener('click', openNewSessionSheet);
|
|
13769
14171
|
$('new-session-cancel').addEventListener('click', () => { $('new-session-sheet').hidden = true; });
|
|
13770
14172
|
$('new-session-form').addEventListener('submit', createSession);
|
|
14173
|
+
$('new-session-machine')?.addEventListener('change', () => { onPhoneMachineChange().catch(() => { /* expected: onPhoneMachineChange reports its own failures in the hint */ }); });
|
|
14174
|
+
bindPhoneMachineSearchSheet();
|
|
13771
14175
|
$('status-content')?.addEventListener('click', (event) => {
|
|
13772
14176
|
if (event.target.closest('[data-private-scope-retry]')) { refreshPhoneHealth({ force: true }).catch(() => {}); retryPrivateStatePreparation().catch(() => {}); return; } // D-7: an explicit re-check ignores the TTL
|
|
13773
14177
|
handleStatusSummaryClick(event);
|
|
@@ -13858,6 +14262,66 @@
|
|
|
13858
14262
|
if (hash.startsWith('#walle/')) setTab('walle');
|
|
13859
14263
|
}
|
|
13860
14264
|
|
|
14265
|
+
// ── WP-6 item 5 / P-7: the boot-failure seam ───────────────────────────────────────────────────
|
|
14266
|
+
// THE seam: init() below. The first API call of a boot is `prepareMobilePrivateState`'s private
|
|
14267
|
+
// scope read (public/js/private-state-scope.js runPreparation: GET /api/auth/me, GET
|
|
14268
|
+
// /api/instance/manifest, GET /api/auth/me again). When that read does not reach this origin, the
|
|
14269
|
+
// runtime records it in the snapshot as `httpStatus`: 0 for a rejected fetch / aborted body (the
|
|
14270
|
+
// transport outage P-7 is about) and the HTTP status when a response did arrive — so a 5xx from the
|
|
14271
|
+
// relay/tunnel is the same "the box behind this origin is not answering" case, while a 4xx means
|
|
14272
|
+
// this origin IS answering and the phone must not navigate away from it.
|
|
14273
|
+
function phoneBootOriginUnreachable() {
|
|
14274
|
+
const snapshot = mobilePrivateStateRuntime?.snapshot?.() || null;
|
|
14275
|
+
const status = Number(snapshot?.httpStatus);
|
|
14276
|
+
if (status === 0) return true;
|
|
14277
|
+
return Number.isFinite(status) && status >= 500;
|
|
14278
|
+
}
|
|
14279
|
+
|
|
14280
|
+
// The phone's own identity for the failover payloads: the dial-cache row for THIS origin (written by
|
|
14281
|
+
// the last successful /api/machines load), falling back to the origin itself. The cache holds the
|
|
14282
|
+
// five roster fields and no credential, so this is never a secret.
|
|
14283
|
+
function phoneSelfOriginRow() {
|
|
14284
|
+
const origin = String(window.location?.origin || '').replace(/\/+$/, '');
|
|
14285
|
+
const rows = window.CtmPoolFailover?.readPoolOrigins?.() || [];
|
|
14286
|
+
const mine = (Array.isArray(rows) ? rows : []).find((row) => String(row?.origin || '').replace(/\/+$/, '') === origin);
|
|
14287
|
+
return { instanceId: String(mine?.instanceId || ''), displayName: String(mine?.displayName || ''), origin };
|
|
14288
|
+
}
|
|
14289
|
+
|
|
14290
|
+
// Fire-and-forget: boot continues rendering the local shell whatever this decides. It navigates
|
|
14291
|
+
// only when another pool member answers (public/m/pool-failover.js), and asks first when this phone
|
|
14292
|
+
// still holds unsent outbox items — the outbox is per origin and would otherwise be stranded.
|
|
14293
|
+
function attemptPhonePoolFailover() {
|
|
14294
|
+
const failover = window.CtmPoolFailover;
|
|
14295
|
+
if (!failover?.failoverOnBootFailure) return null;
|
|
14296
|
+
// Offline means no member can answer: probing would burn four seconds per member to prove nothing.
|
|
14297
|
+
if (typeof navigator !== 'undefined' && navigator.onLine === false) return null;
|
|
14298
|
+
if (!phoneBootOriginUnreachable()) return null;
|
|
14299
|
+
return failover.failoverOnBootFailure({
|
|
14300
|
+
// The leased outbox is not readable yet, so this is the content-free presence marker app.js keeps
|
|
14301
|
+
// in plain localStorage; the in-memory length is ORed in so the prompt can never under-report.
|
|
14302
|
+
hasUnsentOutbox: !!failover.readOutboxPendingMarker?.(phonePlainStorage()) || state.remoteOutbox.length > 0,
|
|
14303
|
+
selfInstanceId: phoneSelfOriginRow().instanceId,
|
|
14304
|
+
renderList: (html) => { mountPhonePoolOriginsList(html); },
|
|
14305
|
+
}).catch(() => null); // HANDLE: a failover that cannot even probe leaves the user on the boot screen
|
|
14306
|
+
}
|
|
14307
|
+
|
|
14308
|
+
// The "no member answered" roster, mounted into the phone's own status surface (the P-7 list).
|
|
14309
|
+
function mountPhonePoolOriginsList(html) {
|
|
14310
|
+
const host = $('pool-failover-list') || $('pool-from-banner');
|
|
14311
|
+
if (!host) return null;
|
|
14312
|
+
host.innerHTML = String(html || '');
|
|
14313
|
+
return host;
|
|
14314
|
+
}
|
|
14315
|
+
|
|
14316
|
+
// AC-6e: arriving here with ?poolFrom=<id> means another member handed this phone over. Both names
|
|
14317
|
+
// come from the stored roster; every string is escaped inside the module.
|
|
14318
|
+
function showPhonePoolFromBanner() {
|
|
14319
|
+
const failover = window.CtmPoolFailover;
|
|
14320
|
+
if (!failover?.showPoolFromBanner) return null;
|
|
14321
|
+
const self = phoneSelfOriginRow();
|
|
14322
|
+
return failover.showPoolFromBanner({ self, selfInstanceId: self.instanceId });
|
|
14323
|
+
}
|
|
14324
|
+
|
|
13861
14325
|
async function init() {
|
|
13862
14326
|
const operation = {};
|
|
13863
14327
|
mobilePrivatePreparationOperation = operation;
|
|
@@ -13885,7 +14349,14 @@
|
|
|
13885
14349
|
state.sessionsLoaded = true;
|
|
13886
14350
|
if (!preparationRetry.isPending()) setConnection('offline', state.auth?.authenticated ? 'Private data paused' : 'Sign in');
|
|
13887
14351
|
render();
|
|
13888
|
-
|
|
14352
|
+
// P-7 (WP-6 item 5): the boot API call did not reach this origin. A scheduled retry means the
|
|
14353
|
+
// origin is answering (the runtime asked us to come back), so only an unreachable origin tries
|
|
14354
|
+
// the other pool members. Nothing here blocks the local shell: the switch is a navigation.
|
|
14355
|
+
if (!preparationRetry.isPending()) attemptPhonePoolFailover();
|
|
14356
|
+
}
|
|
14357
|
+
// AC-6e: if we arrived on a ?poolFrom=<id> hand-over, say who handed us over — over the shell, on
|
|
14358
|
+
// every tab, whether or not the private lease came up.
|
|
14359
|
+
showPhonePoolFromBanner();
|
|
13889
14360
|
if ('serviceWorker' in navigator && !window.__CTM_MOBILE_TEST_DISABLE_SW) {
|
|
13890
14361
|
registerMobileServiceWorker().catch(() => {});
|
|
13891
14362
|
}
|
|
@@ -13993,6 +14464,28 @@
|
|
|
13993
14464
|
itemsContext: state.skillPicker.itemsContext ? { ...state.skillPicker.itemsContext } : null,
|
|
13994
14465
|
}),
|
|
13995
14466
|
selectMobileSkillItemForTest: (index, control) => selectMobileSkillItem(index, control),
|
|
14467
|
+
// WP-5 (S-11/S-16 AC-5a/AC-5c): the machine surfaces, so the phone spec can assert the same
|
|
14468
|
+
// contract the desktop spec does — zero machines renders nothing, the create payload carries
|
|
14469
|
+
// `machine`, and a remote row carries the chip.
|
|
14470
|
+
loadMachines,
|
|
14471
|
+
openNewSessionSheet,
|
|
14472
|
+
createSession,
|
|
14473
|
+
machineOptionsHtml: (machines, now) => phoneMachineOptionsHtml(machines, now),
|
|
14474
|
+
machinePickerMode: (machines) => phoneMachinePickerMode(machines),
|
|
14475
|
+
machineSearchSheetHtml: (machines, query, selectedId, now) => window.CtmPhoneMachines?.sheetListHtml?.(machines, query, selectedId, now) || '',
|
|
14476
|
+
// WP-6: the Machines page itself now lives in public/m/machines.js (its presenters are exported
|
|
14477
|
+
// there); this renders the page's list for a given registry, so the phone spec can still assert
|
|
14478
|
+
// the card contract from the app realm without reaching into the module.
|
|
14479
|
+
machinesCardHtml: (machines) => {
|
|
14480
|
+
const surface = phoneMachines();
|
|
14481
|
+
if (!surface) return '';
|
|
14482
|
+
if (Array.isArray(machines)) surface.state.machines = machines.map((machine) => ({ ...machine }));
|
|
14483
|
+
return surface.listHtml();
|
|
14484
|
+
},
|
|
14485
|
+
machineState: () => state.machines.map((machine) => ({ ...machine })),
|
|
14486
|
+
sessionRowHtml: (card) => sessionRow(card),
|
|
14487
|
+
sameRepoProject: (localProjects, localCwd, boxRows) => phoneSameRepoProject(localProjects, localCwd, boxRows),
|
|
14488
|
+
machineChipHtml: (card) => phoneMachineChip(card),
|
|
13996
14489
|
};
|
|
13997
14490
|
}
|
|
13998
14491
|
|