- Methods: All Methods Documented Methods Hide Methods
- Source: Display Source Hide Source
- Variables: Show Variables Hide Variables
Class ::nsmcp::QuestionService
::nsmcp::QuestionServicecreate ... \
[ -backend:required backend:required ] \
[ -backendErrors (default "") ] \
[ -backends (default "") ] \
[ -evidenceDirectory (default "") ] \
[ -lifetime (default "3600") ] \
[ -maxBytes:integer (default "2097152") ] \
[ -maxConversations:integer (default "20") ] \
[ -maxEvidenceBytes:integer (default "16777216") ] \
[ -maxTurns:integer (default "20") ] \
[ -mutex mutex ] \
[ -questionLog:boolean (default "true") ] \
[ -queue:required queue:required ] \
[ -store:required store:required ]
Defined in /usr/local/ns/tcl/nsmcp/question-service.tcl
Class Relations
::nx::Class create ::nsmcp::QuestionService \ -superclass ::nsmcp::ObjectMethods (to be applied on the object)
setup (scripted, public)
nsmcp::QuestionServicesetup moduleDirectory
Configure question backends and the question service at startup. Honor enableQuestions, validate the tool registry and credentials, and establish the configured provider implementations.
- Parameters:
- moduleDirectory (required)
- Installed module directory used to resolve backend helpers.
- Returns:
- The startup setup result, or an empty result when questions are disabled.
- Testcases:
- No testcase defined.
if {![ns_config -bool ${:section} enableQuestions false]} { return } if {![nsf::object::exists ::nsmcp::tools]} { error "Questions require the configured nsmcp HTTP endpoint" } set backend [ns_config ${:section} questionBackend codex] if {$backend ni {codex chat-completions}} { error "Unsupported question backend: $backend" } set lifetime [ns_baseunit -time [ns_config ${:section} questionLifetime 1h]] if {$lifetime < 1} { error "questionLifetime must be positive" } set maxBytes [ns_baseunit -size [ns_config ${:section} questionMaxBytes 2MiB]] set maxEvidence [ns_baseunit -size [ns_config ${:section} questionMaxEvidenceBytes 16MiB]] if {$maxBytes<65536 || $maxEvidence<1024} { error "questionMaxBytes requires at least 64KiB; questionMaxEvidenceBytes requires at least 1KiB" } set evidenceDirectory [ns_config ${:section} questionEvidenceDirectory [file join [ns_info home] data nsmcp-question-evidence [ns_info server]]] if {[file pathtype $evidenceDirectory] ne "absolute"} { error "questionEvidenceDirectory must be absolute" } :createBackend $backend $moduleDirectory ::nsmcp::questionBackend set backends [dict create $backend ::nsmcp::questionBackend] set errors {} foreach candidate {codex chat-completions} { if {$candidate eq $backend} { continue } set object ::nsmcp::questionBackend_[string map {- _} $candidate] if {[catch { :createBackend $candidate $moduleDirectory $object } reason]} { if {[nsf::object::exists $object]} { $object destroy } dict set errors $candidate [string range $reason 0 1023] } else { dict set backends $candidate $object } } :create ::nsmcp::questions -backend ::nsmcp::questionBackend -backends $backends -backendErrors $errors -questionLog [ns_config -bool ${:section} questionLog true] -store nsmcp:questions:[ns_info server] -queue nsmcp-questions-[ns_info server] -lifetime $lifetime -maxBytes $maxBytes -maxEvidenceBytes $maxEvidence -evidenceDirectory $evidenceDirectory -maxConversations [ns_config -int -min 1 ${:section} questionMaxConversations 20] -maxTurns [ns_config -int -min 1 ${:section} questionMaxTurns 20]Methods (to be applied on instances)
cancel (scripted, public)
<instance of nsmcp::QuestionService> cancel context handle
Cancel the active turn of an owned conversation. Update queued or running state under the mutex so subsequent callbacks observe cancellation.
- Parameters:
- context (required)
- Authenticated request context carrying the principal, grants and selected server.
- handle (required)
- Retained job or conversation handle; ownership and expiry checks still apply.
- Returns:
- The cancellation/conversation result.
- Testcases:
- No testcase defined.
ns_mutex lock ${:mutex} try { set record [:owned $context $handle] if {[dict get $record status] eq "queued"} { dict set record status cancelled set last [lindex [dict get $record turns] end] dict set last status cancelled dict set record turns [lreplace [dict get $record turns] end end $last] nsv_set ${:store} $handle $record } elseif {[dict get $record status] eq "running"} { dict set record status cancelling nsv_set ${:store} $handle $record } } finally { ns_mutex unlock ${:mutex} } return [:poll $context $handle]choices (scripted, public)
<instance of nsmcp::QuestionService> choices
List configured question backend choices. Describe available providers and configured failures without contacting a provider.
- Returns:
- A dictionary containing the default backend and backend metadata.
- Testcases:
- No testcase defined.
set providers ${:backends} dict set providers [:defaultBackend] ${:backend} set choices {} dict for {name object} $providers { dict set choices $name [dict merge [$object describe] {available true}] } dict for {name reason} ${:backendErrors} { if {![dict exists $choices $name]} { dict set choices $name [dict create available false reason $reason] } } return [dict create default [:defaultBackend] backends $choices]continue (scripted, public)
<instance of nsmcp::QuestionService> continue context handle \ turn [ peerAddress ]
Resume an owned conversation from its retained continuation state. Derive the job and window from server-owned state and recheck ownership and turn validity before queuing work.
- Parameters:
- context (required)
- Authenticated request context carrying the principal, grants and selected server.
- handle (required)
- Retained job or conversation handle; ownership and expiry checks still apply.
- turn (required)
- Retained turn identifier, used to reject superseded callbacks.
- peerAddress (optional)
- Direct connection peer supplied by the authenticated HTTP adapter.
- Returns:
- The rooted JSON conversation status returned by send.
- Testcases:
- No testcase defined.
# Derive handles from owned server state; the browser cannot substitute # another job or change its original time window. send rechecks the turn # under its queue lock, so a stale button cannot resume an older turn. ns_mutex lock ${:mutex} try { set record [:owned $context $handle] if {$turn eq "" || [dict get $record current] ne $turn} { error "The conversation has moved on; refresh its status before continuing" } set jobs [:continuationJobs $record] if {$jobs eq {}} { error "No retained background jobs are available to continue" } set original [dict get [lindex [dict get $record turns] end] question] set sourceTurn $turn foreach previous [dict get $record turns] { set found false foreach job $jobs { if {[dict exists $previous jobs $job]} { set found true break } } if {$found} { set original [dict get $previous question] set sourceTurn [dict get $previous id] break } } } finally { ns_mutex unlock ${:mutex} } set excerpt [string range $original 0 1023] if {[string length $original]>1024} { append excerpt " (original question excerpt)" } set question "Continue the retained analysis and finish the answer. Retrieve only these existing background jobs with log_results; keep polling them if needed. Do not start duplicate scans or reinterpret relative times: use the jobs' original fixed time windows. If a job has expired or failed, report that rather than starting a replacement.\nJob handles: [join $jobs {, }].\nOriginal request (context only): $excerpt" return [:send $context {Continue the previous analysis.} $handle {} {} $peerAddress $turn $question $sourceTurn]describe (scripted, public)
<instance of nsmcp::QuestionService> describe
Describe the default backend and service evidence budgets. Merge provider metadata with this service's configured memory and evidence limits.
- Returns:
- The service metadata dictionary.
- Testcases:
- No testcase defined.
return [dict merge [${:backend} describe] [dict create maxBytes ${:maxBytes} maxEvidenceBytes ${:maxEvidenceBytes}]]event (scripted, public)
<instance of nsmcp::QuestionService> event handle turn event
Record a backend event for a specific retained turn. Serialize updates and discard events for expired or superseded turns.
- Parameters:
- handle (required)
- Retained job or conversation handle; ownership and expiry checks still apply.
- turn (required)
- Retained turn identifier, used to reject superseded callbacks.
- event (required)
- Backend event dictionary for the retained question turn.
- Returns:
- Whether the event was accepted for the active turn.
- Testcases:
- No testcase defined.
ns_mutex lock ${:mutex} try { if {![nsv_exists ${:store} $handle]} { return false } set record [nsv_get ${:store} $handle] if {[dict get $record current] ne $turn} { return false } set continue [expr {[dict get $record status] ne "cancelling"}] if {[dict get $event type] eq "check"} { return $continue } set last [lindex [dict get $record turns] end] switch -- [dict get $event type] { model { dict set last model [string range [dict get $event model] 0 255] } questionUsage { set raw [dict get $event usage] set usage [::nsmcp::questionUsageCounters $raw { inputTokens inputTokens outputTokens outputTokens totalTokens totalTokens cachedInputTokens cachedInputTokens cacheWriteInputTokens cacheWriteInputTokens reasoningOutputTokens reasoningOutputTokens }] foreach key {scope source reason} { if {[dict exists $raw $key]} { dict set usage $key [string range [dict get $raw $key] 0 63] } } dict set usage available [expr {[dict exists $raw available] && [string is boolean -strict [dict get $raw available]] && [dict get $raw available]}] dict set last questionUsage $usage } usage { # Only the adapter-neutral allowlist reaches logs/storage. set raw [dict get $event usage] set usage [::nsmcp::questionUsageCounters $raw { inputTokens inputTokens outputTokens outputTokens totalTokens totalTokens cachedInputTokens cachedInputTokens uncachedInputTokens uncachedInputTokens cacheWriteInputTokens cacheWriteInputTokens reasoningOutputTokens reasoningOutputTokens apiRequests apiRequests requestsWithUsage requestsWithUsage }] foreach key {scope source} { if {[dict exists $raw $key]} { dict set usage $key [string range [dict get $raw $key] 0 63] } } if {[dict exists $raw complete] && [string is boolean -strict [dict get $raw complete]]} { dict set usage complete [expr {!![dict get $raw complete]}] } dict set last usage $usage } text { dict append last answer [dict get $event text] } job { set key [dict get $event jobHandle] if {![dict exists $last jobs $key] && [dict exists $last jobs] && [dict size [dict get $last jobs]]>=128} { error "Question background job limit reached" } dict set last jobs $key $event } tool { dict lappend last tools [:toolEvidence $handle $event] } state { set state [dict get $event state] if {[string bytelength $state]>${:maxBytes}} { error "Question continuation size limit reached; start a new conversation" } dict set record state [zlib compress [encoding convertto utf-8 $state]] dict set record stateEncoding zlib-tcl-v1 } } dict set record turns [lreplace [dict get $record turns] end end $last] if {[string bytelength $record] > ${:maxBytes}} { error "Conversation size limit reached (${:maxBytes} bytes); start a new conversation" } nsv_set ${:store} $handle $record return $continue } finally { ns_mutex unlock ${:mutex} }evidence (scripted, public)
<instance of nsmcp::QuestionService> evidence context handle id
Retrieve retained tool evidence for an owned conversation. Validate the evidence identifier, confirm it belongs to a retained turn and read only the bounded private evidence file.
- Parameters:
- context (required)
- Authenticated request context carrying the principal, grants and selected server.
- handle (required)
- Retained job or conversation handle; ownership and expiry checks still apply.
- id (required)
- Private evidence identifier already referenced by an owned turn.
- Returns:
- A rooted JSON object containing the retained result text.
- Testcases:
- No testcase defined.
ns_mutex lock ${:mutex} try { set record [:owned $context $handle] if {![regexp {^[a-f0-9]{32}$} $id]} { error "Unknown evidence" } set found false foreach turn [dict get $record turns] { foreach tool [dict get $turn tools] { if {[dict exists $tool evidenceHandle] && [dict get $tool evidenceHandle] eq $id} { set found true } } } if {!$found} { error "Unknown evidence" } set path [file join ${:evidenceDirectory} $handle $id.json] if {[file type $path] ne "file" || [file size $path]>${:maxEvidenceBytes}} { error "Evidence unavailable" } set channel [open $path r] try { fconfigure $channel -encoding utf-8 -translation lf set result [read $channel] } finally { close $channel } return [list {} object [list result string $result]] } finally { ns_mutex unlock ${:mutex} }forget (scripted, public)
<instance of nsmcp::QuestionService> forget context handle
Remove an owned conversation and its retained evidence. Reject active work when required and clean up private evidence with the retained conversation state.
- Parameters:
- context (required)
- Authenticated request context carrying the principal, grants and selected server.
- handle (required)
- Retained job or conversation handle; ownership and expiry checks still apply.
- Returns:
- The deletion result.
- Testcases:
- No testcase defined.
ns_mutex lock ${:mutex} try { set record [:owned $context $handle] if {[dict get $record status] in {queued running cancelling}} { error "Cancel the running question before starting a new conversation" } :drop $handle } finally { ns_mutex unlock ${:mutex} } return {{} object {status string forgotten}}poll (scripted, public)
<instance of nsmcp::QuestionService> poll context handle
Describe an owned conversation and its retained turns. Apply ownership and expiry checks before exposing bounded question, tool, evidence and usage metadata.
- Parameters:
- context (required)
- Authenticated request context carrying the principal, grants and selected server.
- handle (required)
- Retained job or conversation handle; ownership and expiry checks still apply.
- Returns:
- A rooted JSON conversation-status object.
- Testcases:
- No testcase defined.
ns_mutex lock ${:mutex} try { set record [:owned $context $handle] } finally { ns_mutex unlock ${:mutex} } set canContinue [expr {[llength [:continuationJobs $record]]>0 && [llength [dict get $record turns]]<${:maxTurns}}] set turns {} set i 0 foreach entry [dict get $record turns] { set tools {} set j 0 foreach call [dict get $entry tools] { lappend tools $j object [list name string [dict get $call name] status string [dict get $call status] arguments string [dict get $call arguments] result string [dict get $call result] truncated boolean [dict get $call truncated]] set timing [::nsmcp::questionToolTiming $call] set fields [lindex $tools end] foreach {key type} {durationMs number durationSource string} { if {[dict exists $timing $key]} { lappend fields $key $type [dict get $timing $key] } else { lappend fields $key null {} } } set tools [lreplace $tools end end $fields] if {[dict exists $call evidenceHandle]} { set fields [lindex $tools end] lappend fields evidenceHandle string [dict get $call evidenceHandle] set tools [lreplace $tools end end $fields] } incr j } set jobs {} set k 0 if {[dict exists $entry jobs]} { dict for {jobHandle job} [dict get $entry jobs] { set fields [list jobHandle string $jobHandle name string [dict get $job name] status string [dict get $job status]] if {[dict exists $job progress]} { set progress {} foreach key {wallMs examinedBytes sourceBytes scanBudgetBytes updatedAt filesCompleted filesProcessed filesSelected filesDiscovered filesSkipped filesScanned filesPending timeIndexSkippedBytes timeIndexValidationBytes selectedFromOffset selectedToOffset selectionProbeBytes aggregationDiskBytes aggregationDiskBudgetBytes aggregationPoolWaitCount aggregationPoolWaitMilliseconds aggregationPoolWaitLimitMilliseconds currentFileIndex currentFileNumber currentFileSize currentFileOffset currentFileSnapshotBytes} { if {[dict exists $job progress $key]} { set value [dict get $job progress $key] lappend progress $key [expr {$value eq "" ? "null" : "number"}] $value } } foreach key {phase offsetBasis scanStatus completionSummary aggregationBackend aggregationStatus aggregationFailurePhase aggregationFailureClass aggregationDiagnosticId} { if {[dict exists $job progress $key]} { lappend progress $key string [dict get $job progress $key] } } if {[dict exists $job progress currentFileCompressed]} { lappend progress currentFileCompressed boolean [dict get $job progress currentFileCompressed] } if {[dict exists $job progress truncatedByLimits]} { lappend progress truncatedByLimits boolean [dict get $job progress truncatedByLimits] } lappend fields progress object $progress } lappend jobs $k object $fields incr k } } lassign [:questionPresentation $record $entry] displayed editable lappend turns $i object [list id string [dict get $entry id] question string $displayed editQuestion string $editable answer string [dict get $entry answer] status string [dict get $entry status] elapsedMs [expr {[dict exists $entry elapsedMs] ? "number" : "null"}] [:dict getdef $entry elapsedMs {}] error string [dict get $entry error] tools array $tools jobs array $jobs canContinue boolean [expr {$canContinue && [dict get $entry id] eq [dict get $record current]}]] incr i } set backendName [:defaultBackend] if {[dict exists $record backend]} { set backendName [dict get $record backend] } return [list {} object [list conversation string $handle server string [dict get $record server] backend string $backendName status string [dict get $record status] expires number [dict get $record expires] turns array $turns]]run (scripted, public)
<instance of nsmcp::QuestionService> run handle turn
Execute a queued conversation turn. Resolve retained owner/provider state, invoke the selected backend and publish completion, failure or cancellation under the mutex.
- Parameters:
- handle (required)
- Retained job or conversation handle; ownership and expiry checks still apply.
- turn (required)
- Retained turn identifier, used to reject superseded callbacks.
- Returns:
- The worker callback result; conversation state retains the outcome.
- Testcases:
- No testcase defined.
ns_mutex lock ${:mutex} try { if {![nsv_exists ${:store} $handle]} { return } set record [nsv_get ${:store} $handle] if {[dict get $record current] ne $turn} { return } if {[dict get $record status] in {cancelling cancelled}} { set skip true } else { set skip false dict set record status running nsv_set ${:store} $handle $record } set state [dict get $record state] if {[dict exists $record stateEncoding]} { set state [encoding convertfrom utf-8 [zlib decompress $state]] } set backendName [:defaultBackend] if {[dict exists $record backend]} { set backendName [dict get $record backend] } set entry [lindex [dict get $record turns] end] set question [expr {[dict exists $entry backendQuestion] ? [dict get $entry backendQuestion] : [dict get $entry question]}] } finally { ns_mutex unlock ${:mutex} } # A timed-out turn may have started a job without publishing provider # history. Carry its handle explicitly into the next question. if {[dict exists $record browserTimezone]} { append question "\nBrowser timezone context: [dict get $record browserTimezone]. Use this timezone for unspecified time windows and displayed times, unless the question explicitly requests another timezone." } set pendingJobs {} foreach previous [dict get $record turns] { if {![dict exists $previous jobs]} { continue } dict for {jobHandle job} [dict get $previous jobs] { dict set job needsRetrieval [expr {[dict get $previous status] in {failed cancelled} || [dict get $job status] in {queued running}}] dict set pendingJobs $jobHandle $job } } dict for {jobHandle job} $pendingJobs { if {[dict get $job needsRetrieval]} { append question "\nExisting background jobHandle $jobHandle ([dict get $job name]); retrieve it with log_results rather than starting it again." } } set error {} set status cancelled if {!$skip} { if {[catch { [:provider $backendName] run $question $state [list [self] event $handle $turn] } status]} { set error $status set status failed } } ns_mutex lock ${:mutex} try { if {![nsv_exists ${:store} $handle]} { return } set record [nsv_get ${:store} $handle] if {[dict get $record current] ne $turn} { return } if {[dict get $record status] eq "cancelling"} { set status cancelled set error {} } dict set record status $status set last [lindex [dict get $record turns] end] dict set last status $status dict set last error $error if {[dict exists $last submittedMs]} { dict set last elapsedMs [expr {max(0,[clock milliseconds]-[dict get $last submittedMs])}] } :logQuestion finished $record $last dict set record turns [lreplace [dict get $record turns] end end $last] nsv_set ${:store} $handle $record } finally { ns_mutex unlock ${:mutex} }send (scripted, public)
<instance of nsmcp::QuestionService> send context question \ [ handle ] [ backendName ] [ browserTimezone ] [ peerAddress ] \ [ expectedTurn ] [ backendQuestion ] [ continuedFrom ]
Queue a question or follow-up with optional browser timezone context. Retain authenticated ownership, backend selection and turn guards when queuing asynchronous provider work. Keep the displayed question separate from server-generated continuation instructions.
- Parameters:
- context (required)
- Authenticated request context carrying the principal, grants and selected server.
- question (required)
- Question text supplied to the selected provider.
- handle (optional)
- Existing conversation handle, or empty to create a new conversation.
- backendName (optional)
- Backend selection for a new conversation; empty retains the configured default.
- browserTimezone (optional)
- Validated browser timezone used unless the question overrides it; empty retains prior context or UTC.
- peerAddress (optional)
- Direct connection peer supplied by the authenticated HTTP adapter.
- expectedTurn (optional)
- Optional internal guard against continuing a superseded turn.
- backendQuestion (optional)
- Optional server-generated backend task, separate from the displayed question.
- continuedFrom (optional)
- Owned source-turn identifier when editing the original request.
- Returns:
- Rooted JSON triples describing the queued conversation.
- Testcases:
- No testcase defined.
if {$browserTimezone ne "" && (![regexp {^[A-Za-z0-9_+:/-]{1,128}$} $browserTimezone] || [catch { clock format 0 -timezone $browserTimezone }])} { error "Invalid browser timezone" } if {$peerAddress ne "" && ![ns_ip valid $peerAddress]} { error "Invalid question peer address" } set question [string trim $question] if {$question eq "" || [string bytelength $question] > 16384} { error "Supply a question up to 16 KiB" } if {$backendQuestion ne {} && ([string bytelength $backendQuestion]>16384 || $expectedTurn eq {} || $continuedFrom eq {})} { error "Invalid internal continuation task" } ns_mutex lock ${:mutex} try { if {$handle eq ""} { :cleanup foreach key [nsv_array names ${:store}] { set old [nsv_get ${:store} $key] if {[dict get $old expires] <= [clock seconds] && [dict get $old status] ni {queued running cancelling}} { :drop $key } } if {[llength [nsv_array names ${:store}]] >= ${:maxConversations}} { error "Conversation limit reached; remove an old conversation" } if {$backendName eq ""} { set backendName [:defaultBackend] } :provider $backendName set handle [ns_crypto::randombytes -encoding hex 32] set record [dict create principal [$context principal] server [$context server] backend $backendName status idle state {} turns {}] } else { set record [:owned $context $handle] if {![dict exists $record backend]} { dict set record backend [:defaultBackend] } if {$backendName ne "" && $backendName ne [dict get $record backend]} { error "Changing backend requires a new conversation" } } if {$backendQuestion ne {}} { set ownedSource false foreach entry [dict get $record turns] { if {[dict get $entry id] eq $continuedFrom} { set ownedSource true break } } if {!$ownedSource} { error "Continuation source is not in this conversation" } } if {$expectedTurn ne "" && [dict get $record current] ne $expectedTurn} { error "The conversation has moved on; refresh its status before continuing" } if {[dict get $record status] in {queued running cancelling}} { error "A question is already running in this conversation" } if {[llength [dict get $record turns]] >= ${:maxTurns}} { error "Conversation turn limit reached; start a new conversation" } if {$browserTimezone ne ""} { dict set record browserTimezone $browserTimezone } set turn [ns_crypto::randombytes -encoding hex 16] dict set record current $turn dict set record status queued dict set record expires [expr {[clock seconds]+int(${:lifetime})}] set description [[:provider [dict get $record backend]] describe] set model {} if {[dict exists $description model]} { set model [dict get $description model] } set last [dict create id $turn conversation $handle question $question answer {} status queued tools {} error {} peerAddress $peerAddress peerSource [expr {$peerAddress eq "" ? "unavailable" : "direct"}] submittedAt [clock seconds] submittedMs [clock milliseconds] requestedModel $model] if {$backendQuestion ne {}} { dict set last backendQuestion $backendQuestion dict set last continuedFrom $continuedFrom } if {[dict exists $description backend] && [dict get $description backend] eq "codex"} { dict set last questionUsage {source codex scope turn available false reason no-provider-usage} } dict lappend record turns $last :logQuestion submitted $record $last nsv_set ${:store} $handle $record try { ns_job queue -detached ${:queue} [list [self] run $handle $turn] } on error {message options} { dict set record status failed set last [lindex [dict get $record turns] end] dict set last status failed dict set last error "Unable to queue the question" dict set record turns [lreplace [dict get $record turns] end end $last] nsv_set ${:store} $handle $record :logQuestion finished $record $last return -options $options $message } } finally { ns_mutex unlock ${:mutex} } return [:poll $context $handle]usageSummary (scripted, public)
<instance of nsmcp::QuestionService> usageSummary
Summarize retained finished-question token records for this server. Keep unknown usage explicit and exclude cumulative provider snapshots from summed turn usage. Apply the retained UTC-day and read bounds.
- Returns:
- A dictionary of UTC-day, backend/model and principal rollups, complete/partial counters, missing counts and read limits.
- Testcases:
- No testcase defined.
ns_mutex lock ${:mutex} try { set result [::nsmcp::usageRead [file join ${:evidenceDirectory} token-usage.jsonl] [ns_info server]] dict set result loggingEnabled ${:questionLog} set failures 0 if {[nsv_exists ${:store}:usage writeFailures]} { set failures [nsv_get ${:store}:usage writeFailures] } dict set result processWriteFailures $failures return $result } finally { ns_mutex unlock ${:mutex} }
- Methods: All Methods Documented Methods Hide Methods
- Source: Display Source Hide Source
- Variables: Show Variables Hide Variables
![[i]](/resources/acs-subsite/ZoomIn16.gif)